Skip to main content
Glama

podcast_server

A self-hosted podcast host with no dashboard. Your AI agent is the interface. You're the approver.

License: BSL 1.1 Python 3.11+ MCP native Built with FastMCP uv Ruff Cloudflare R2 Podcasting 2.0

X LinkedIn YouTube Website

What it is · How it works · Quick start · MCP tools · Adopting guide · Agent guide


What it is

Podcast hosts charge a monthly fee for three things: storage for the audio, a database of episodes, and an RSS feed, all behind a web dashboard. podcast_server keeps those three on infrastructure you own and replaces the dashboard with MCP tools. The "user interface" is whichever AI agent you already use: Claude, Cursor, Codex, or your own.

You talk to your agent: "The interview with Ada is ready in Descript. Prep it." The agent stages the episode, drafts titles and show notes in your show's voice, and stops. You approve. The agent publishes, uploads the video, sets the thumbnail, posts the announcement, drafts copies of the companion blog post on dev.to and Substack (your site stays the original), and tells you a week later how it's doing.

flowchart LR
    subgraph hosted["A hosting service"]
        direction TB
        Y1([You]) -->|click through| D[Their web dashboard]
        D --> S1[(Their storage)]
        D --> C1[(Their database)]
        D --> F1[Their feed URL]
    end
    subgraph self["podcast_server"]
        direction TB
        Y2([You]) <-->|chat and approve| A[Your AI agent]
        A -->|MCP tools| E[podcast_server]
        E --> S2[(Your R2 bucket)]
        E --> C2[(Your SQLite catalog)]
        E --> F2[Feed on your domain]
    end
    hosted ~~~ self

How to place it:

Question

Answer

What job does it do?

Podcast hosting and publishing: storage, feed, catalog, distribution, and analytics

Where does it run?

On your machine, against services you own: Cloudflare R2, your domain, and optionally a git-deployed website

How do you operate it?

Through an AI agent over MCP. There is no web UI.

Who decides?

You. The agent drafts and executes; nothing public happens without your approval.

Closest category

A headless podcast host: like a headless CMS, where the missing "head" is an agent

What it isn't:

  • Not an editor. Edit in Descript, or anything that exports an MP3. The engine takes it from there.

  • Not an AI that makes your show. The engine has no taste, on purpose. Your playbook holds the voice and standards, the agent applies them, and you approve.

  • Not a hosted service. Nobody runs it for you, and there's no account to sign up for.

  • Not a wrapper around someone else's product. It is the system of record: the catalog, the episode GUIDs, the published audio, and the log of every public action live here.

New here? Start with ADOPTING.md, a step-by-step guide to running your own show, including migrating from another host without losing subscribers.


Related MCP server: sitedrop

How it works

Three layers, two people

flowchart TB
    YOU([You: taste and approval]) <--> AGENT([Your AI agent: drafts and executes])
    AGENT -->|reads first| SHOW
    AGENT -->|calls| SURFACE

    subgraph SHOW["Show layer: your private DATA_DIR"]
        PB[PLAYBOOK.md: voice, title rules, workflows]
        TPL[templates: social posts, thumbnail style]
        CAT[(catalog.db and logs of every public action)]
    end

    subgraph SURFACE["Surface: the MCP server"]
        TOOLS[typed tools with safety annotations]
        RULES[operating rules sent on connect, AGENTS.md]
    end

    subgraph ENGINE["Engine: show-neutral Python"]
        CORE[core: catalog, storage, feed, site, discovery checks, art, doctor, capabilities]
        IO[importers and exporters: RSS, Descript, YouTube, X, Instagram, dev.to, Substack]
        MET[metrics: OP3, Cloudflare, X, Instagram, YouTube, dev.to, Substack]
    end

    subgraph OWNED["Services you own"]
        R2[(Cloudflare R2 on your domain)]
        WEB[your website repo]
        PLAT[YouTube, X, Instagram, dev.to, and Substack accounts]
        HF[(private Hugging Face backup)]
    end

    SURFACE --> ENGINE
    CORE --> CAT
    CORE --> R2
    CORE --> WEB
    CORE --> HF
    IO --> PLAT
    MET --> PLAT

Layer

Its job

Where it lives

Engine

The hands: does each job correctly and refuses unsafe ones

this repo: core/, importers/, exporters/, metrics/

Surface

The controls: typed tools, safety annotations, operating rules

mcp_server/server.py, AGENTS.md

Show

The taste and the memory: voice, templates, catalog, logs

your private DATA_DIR (template: examples/show-data/)

The engine and surface know nothing about any particular show. That's why this repo can be public while your show data stays private, and why one checkout can run several shows (one .env each).

A release, end to end

sequenceDiagram
    actor You
    participant Agent as Your AI agent
    participant MCP as podcast_server
    participant Svc as Your services
    You->>Agent: The episode with Ada is ready in Descript
    Agent->>MCP: podcast_stage_from_descript
    MCP->>Svc: download the master audio
    MCP-->>Agent: measured audio, chapters, draft show notes
    Agent->>Agent: titles, notes, and a discovery brief in the playbook's voice
    Agent->>MCP: podcast_render_thumbnail and podcast_render_episode_art
    Agent-->>You: review package: titles, notes, brief next to each channel's fields, art
    You->>Agent: approved, with edits
    Agent->>MCP: podcast_stage_discovery
    Agent->>MCP: podcast_publish with transcript and episode art
    MCP->>MCP: check slug, GUID, exclusions, discovery fields, files, and audio before any upload
    MCP->>Svc: upload audio, transcript, chapters, and art to R2, rebuild feed and pages, deploy
    MCP-->>Agent: live URLs and deploy status
    Agent->>MCP: podcast_backup
    Agent-->>You: published, with links

After release: promote and measure

Every public action is logged, and metrics only count what the engine itself put out, so numbers are never mixed with the rest of your accounts:

flowchart LR
    P[published episode] --> V[YouTube upload and thumbnail]
    P --> X[X post]
    P --> IG[Instagram post]
    P --> DT[dev.to and Substack blog drafts]
    V --> L[(logs in DATA_DIR/social)]
    X --> L
    IG --> L
    DT --> L
    L --> C[daily collectors: OP3, Cloudflare, X, Instagram, YouTube, dev.to, Substack]
    C --> M[(metrics.db)]
    M --> D[offline dashboard and metrics tools]
    D -->|"how is the show doing?"| AG([your agent])

Safety lives in the engine, not the prompt

An agent can misread a prompt; it can't get past code. The rules that matter are enforced by the engine itself: a database trigger blocks excluded episodes, publishing checks everything before the first upload, uploads never overwrite, and every public action is logged so a retry can't double-post. On top of that, every tool carries MCP annotations, so your client can run reads freely and ask you before anything public:

Level

What your client can do

Tools

Read-only

run without asking

podcast_status, podcast_capabilities, podcast_inspect, podcast_list_episodes, podcast_get_episode, podcast_draft_social, podcast_metrics_summary, podcast_episode_metrics, and the podcast_list_* tools (Instagram, YouTube, dev.to, Substack)

Local files

run without asking

podcast_stage_discovery, podcast_stage_thumbnail, podcast_render_thumbnail, podcast_render_episode_art, podcast_render_newsletter_preview, podcast_make_gif, podcast_metrics_report, podcast_create_blog, podcast_pull_newsletter

Outside services, additive

run, safe to retry

podcast_stage_from_descript, podcast_rebuild_feed, podcast_upload_media_asset, podcast_crosspost_to_devto, podcast_crosspost_to_substack, and podcast_draft_newsletter (unpublished drafts only), podcast_unschedule_newsletter, podcast_metrics_collect, podcast_backup

Public edit

ask you first; safe to retry

podcast_update_episode, podcast_deploy_site, podcast_set_youtube_thumbnail, podcast_unpublish_devto_article, and (permanent) podcast_delete_instagram_post, podcast_delete_youtube_video, podcast_delete_substack_post

Public release

ask you first; never repeat blindly

podcast_publish, podcast_publish_to_youtube, podcast_post_to_x, podcast_post_to_instagram, podcast_send_newsletter

Design principles

  1. You own the infrastructure. Audio sits in your bucket, the feed on your domain, the catalog in a file you can open. Leaving means copying files, not exporting from someone's dashboard.

  2. The agent operates; you approve. Nothing goes public without your explicit approval of the exact content. When a request is unclear, an identifier doesn't match exactly, or a step would bypass a tool, the agent asks instead of guessing, and it says up front what the engine can't do.

  3. Guarantees live in code. GUIDs are permanent, exclusions are enforced by the database, and checks run before side effects.

  4. Everything is inspectable. SQLite, Markdown, JSON, and append-only JSONL logs. No hidden state.

  5. The engine is show-neutral. Your voice, templates, and data live in your own DATA_DIR, never in this repo.


Features

  • Feed: RSS 2.0 + iTunes + Podcasting 2.0 (<podcast:locked>, <podcast:guid>), exact enclosure byte lengths, Markdown show notes rendered to content:encoded, optional OP3 analytics.

  • Two site modes: none serves the feed from your R2 domain, with no website needed. astro writes one Markdown page per episode into your site repo and pushes only those files.

  • Migration: a GUID-preserving importer for any host's RSS feed, with audio mirrored into R2.

  • Agent surface: MCP tools covering staging (including Descript), publishing, episode edits, deploys, backups, blog posts, social drafts, X and Instagram posts, media uploads, YouTube uploads and thumbnails, and audience metrics. Each tool carries MCP safety annotations (read-only, destructive, idempotent), and the server sends its operating rules to every client on connect, including when to stop and ask the human. podcast_capabilities tells the agent which channels are ready right now, and which requests need the human, so it can show you what's possible before planning the work.

  • Discovery at publish time: each piece of content gets one approved brief (main search phrase, secondary phrases, audience, the question it answers, names people search for), and every publish path checks its own fields against it before anything goes public. That covers the episode title and summary, the blog's meta description, YouTube's title, the first 150 characters of the description, tags, and chapters, alt text and the visible start of an Instagram caption, and alt text on X media. A miss is refused, not warned about. New episodes also carry a transcript (podcast:transcript, which Apple displays), a chapters file, and their own artwork in the feed; YouTube uploads get a caption track and a playlist. Applies to content published from now on; nothing already published is changed.

  • Blog cross-posting: your site keeps the original; the companion blog post goes to dev.to and Substack as unpublished drafts, with site-relative links made absolute. dev.to drafts carry a canonical URL back to your site; Substack has no canonical setting, so its drafts open with an "Originally published at" link. You publish each one on its platform.

  • Instagram posts: approved images go to your Instagram professional account as feed posts, converted to JPEG and staged on your R2 domain for Meta to fetch. A one-time login saves a never-expiring Page token in its own 0600 file. Carousels, Stories, and Reels are planned.

  • Content management: agents can list what's on Instagram, YouTube, dev.to, and Substack and remove one item you named at a time: delete an Instagram post, YouTube video, or Substack post or draft (permanent), or unpublish a dev.to article back to draft (dev.to's API can't delete). Each removal checks the item is yours first, has a dry run, and is logged with what was removed.

  • YouTube Shorts: uploads that are square or vertical and up to 3 minutes become Shorts; with expect_short, the engine measures the clip first (ffprobe, rotation-aware) and refuses one that would land as a regular video.

  • YouTube thumbnails and episode art: your agent renders the art; the engine sets the headline in your show's fonts and colors (auto-fitted, clear of the duration badge, under 2 MB) and pushes it to YouTube after approval. The same art and headline also become the episode's square icon in Apple Podcasts, Spotify, and other apps (2048 px, with your show's wordmark), published with the episode; REQUIRE_EPISODE_IMAGE=true makes it mandatory.

  • Audience metrics: pluggable collectors (OP3 downloads, Cloudflare audio traffic, X, Instagram, and YouTube engagement for the show's own posts and videos, dev.to and Substack reach for the blog posts the engine cross-posted, or your own) feed a local SQLite store, an offline HTML dashboard (an overview plus a tab per platform with every collected number, filtered by range or a stretch you drag across the timeline), and MCP tools agents can query.

  • Safety: see the list below; checks run before any upload.

  • Backups: incremental, to a private Hugging Face dataset.


What You Need

podcast_server doesn't host anything itself. It drives services you own. The pieces below are what a show needs; each links to its own setup docs, which are the authority for that service (and several have their own MCP server your agent can use for setup).

Required

Piece

What it's for

Get set up

A domain

Your audio URLs (e.g. audio.myshow.com) and feed live on it, so you can change hosts without losing subscribers

Any registrar, e.g. Cloudflare Registrar. The domain's DNS must be on Cloudflare for the R2 custom domain (full setup).

Cloudflare R2 bucket

Stores the MP3s and serves them (no egress fees, range requests)

R2 docs: create a bucket, an API token with Object Read & Write, and a custom domain on it. Cloudflare's MCP servers can help an agent do this.

A home for the RSS feed

The URL podcast apps subscribe to

Either nothing extra (SITE_MODE=none uploads the feed to your R2 domain) or a git-deployed static site (SITE_MODE=astro, e.g. Astro on Netlify), which also gets one page per episode.

Cover art

Required by every directory

Square JPEG/PNG, 1400–3000 px, at a public URL. See Apple's requirements.

Directory listings

How listeners find the show

Submit your feed URL once to Apple Podcasts Connect, Spotify for Creators, and Podcast Index (which feeds many smaller apps).

An MCP client and Python tooling

The agent that runs the tools, and the engine's runtime

Any MCP client (Claude Desktop, Cursor, ...), plus uv.

Optional

Piece

Unlocks

Get set up

Descript

Staging episodes straight from a Descript export (chapters from markers)

Export the transcript as Markdown; see ADOPTING.

X developer app + xurl

podcast_post_to_x and X metrics for those posts

An app in the X developer portal (read and write, OAuth 2.0); X bills API use. Steps in ADOPTING.

Instagram professional account (Business or Creator) + a Meta app

podcast_post_to_instagram: image posts to the show's Instagram, and metrics for those posts

A Meta developer app; a never-expiring Page token for the Facebook Page linked to the account (or a Unified login token); your own account needs no App Review. Saved by python -m exporters.instagram login. Steps in ADOPTING.

dev.to API key

podcast_crosspost_to_devto: companion blog posts as dev.to drafts

Generate a key under dev.to → Settings → Extensions; needs a website (SITE_MODE=astro) for the canonical URL. Steps in ADOPTING.

A Substack publication

podcast_crosspost_to_substack: companion blog posts as Substack drafts; podcast_draft_newsletter / podcast_send_newsletter: original newsletter articles, drafted, then scheduled or sent

Substack has no official publishing API: this uses the community python-substack library and your browser session cookie, which controls the whole account and expires. Steps in ADOPTING. For episodes, Substack's own Distribute to Substack reads your feed, no engine changes needed.

YouTube Data + Analytics APIs

podcast_publish_to_youtube, podcast_set_youtube_thumbnail, podcast_list_youtube_videos / podcast_delete_youtube_video (deleting needs the youtube.force-ssl scope), and YouTube metrics for the show's videos; ffmpeg's ffprobe for the optional Shorts check

A Google Cloud OAuth client (Data API, Analytics API). Custom thumbnails need a phone-verified channel.

An image model your agent can call

Thumbnail art (the engine only sets the text)

Whatever your agent supports (e.g. via its own MCP or API tools); put fonts and colors in DATA_DIR/templates/thumbnail_style.json.

OP3

Free, open download analytics (stats are public)

USE_OP3_PREFIX=true and a free API key.

Cloudflare analytics token

Audio-traffic metrics from the edge

A token with Analytics Read on the zone (newer dashboards: a policy scoped to the Domain).

Hugging Face account

Private, incremental backups of audio and catalog

A private dataset; Hugging Face also offers an MCP server.

ADOPTING.md walks through each step in order with the exact settings.

Quick Start

# needs uv: https://docs.astral.sh/uv/  (curl -LsSf https://astral.sh/uv/install.sh | sh)
git clone <this repo> podcast_server && cd podcast_server
uv sync                                       # creates .venv with Python 3.11+ and locked dependencies
cp .env.example .env && chmod 600 .env       # fill in your show's values
uv run python -m core.doctor  # verifies R2, domain, site, artwork, feed, and each optional integration
uv run python -m unittest discover -s tests

ADOPTING.md covers Cloudflare setup, configuration, first publish, and migration in detail.

Configuration

Every setting is documented inline in .env.example.

Group

Variables

Show identity (required)

PODCAST_TITLE, PODCAST_DESCRIPTION, PODCAST_AUTHOR, PODCAST_OWNER_EMAIL, PODCAST_LINK, PODCAST_FEED_URL, PODCAST_IMAGE_URL, EPISODE_GUID_PREFIX

Show identity (optional)

PODCAST_CATEGORY, PODCAST_SUBCATEGORIES, PODCAST_LANGUAGE, PODCAST_EXPLICIT, PODCAST_GUID

Storage (required)

R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET_NAME, R2_PUBLIC_DOMAIN, optional USE_OP3_PREFIX

Website

SITE_MODE (none/astro), WEBSITE_PATH, WEBSITE_RSS_FILE, WEBSITE_EPISODES_DIR, WEBSITE_BLOG_DIR, WEBSITE_BRANCH, FEED_OBJECT_KEY

Show data

DATA_DIR (catalog, transcripts, templates, playbook, backups); DATABASE_PATH and METRICS_DB_PATH to move the two databases elsewhere (default: inside DATA_DIR)

Publishing rules (optional)

REQUIRE_EPISODE_IMAGE (refuse new episodes without their own square art)

Integrations (optional)

DESCRIPT_API_BEARER (or DESCRIPT_API_KEY), YOUTUBE_TOKEN_PATH, FFPROBE_PATH (Shorts check), FFMPEG_PATH (podcast_make_gif), HF_BACKUP_REPO, THUMBNAIL_STYLE, DEFAULT_TAGS

X posting (optional)

X_USERNAME, XURL_APP, XURL_PATH (tokens stay in xurl's own auth file)

Instagram posting (optional)

INSTAGRAM_USERNAME, INSTAGRAM_TOKEN_PATH (token file written by python -m exporters.instagram login)

Blog cross-posting (optional)

BLOG_URL_TEMPLATE (your blog post URL with {slug}), DEVTO_API_KEY, SUBSTACK_PUBLICATION_URL, SUBSTACK_COOKIES_PATH (cookie file written by python -m exporters.substack_post login)

Metrics (optional)

OP3_API_TOKEN (+ OP3_SHOW_UUID to skip the lookup), CLOUDFLARE_API_TOKEN + CLOUDFLARE_ZONE_ID, X via the posting setup (or X_BEARER_TOKEN), X_LOOKBACK_DAYS, Instagram via the posting setup, YouTube via YOUTUBE_TOKEN_PATH (+ $DATA_DIR/youtube_videos.json), dev.to and Substack via the cross-posting setup, METRICS_SOURCES, METRICS_EXTRA_COLLECTORS, METRICS_BACKFILL_DAYS

There are no show values in the code: startup fails and lists any required variable that's missing. PODCAST_SERVER_ENV_FILE=/path/to/show.env selects a different config (one per show).

Your Show's Private Data

Keep DATA_DIR outside this checkout, ideally in its own private repo. examples/show-data/ is a template to copy:

my-show-data/
├── PLAYBOOK.md               # your editorial voice, title rules, workflows (agents read this first)
├── DEPLOYMENT.md             # your infrastructure record: DNS, R2, website, backups, credential locations
├── templates/                # social_long.txt, social_short.txt, thumbnail_style.json, fonts/
├── social/                   # logs of public actions and removals (*_deletions.jsonl, devto_unpublished.jsonl): x_posts.jsonl, instagram_posts.jsonl, devto_posts.jsonl, substack_posts.jsonl, substack_newsletters.jsonl, youtube_uploads.jsonl, youtube_thumbnails.jsonl
├── newsletters/              # original Substack newsletter articles, <slug>.md (title, subtitle, tags frontmatter)
├── youtube_videos.json       # optional: back-catalog videos → episodes, for YouTube metrics
├── thumbnails/               # staged art, rendered thumbnails, and newsletter previews (<slug>-substack.png)
├── media/                    # staged GIFs and images for newsletters and blog posts (podcast_make_gif)
├── excluded_episodes.json    # episodes that must never be (re)published
├── transcripts/              # <slug>.txt
├── catalog.db                # created on first run; don't commit it (back it up instead)
├── metrics.db                # collected audience metrics; safe to delete and re-collect
└── metrics/dashboard.html    # written by `python -m metrics.report`

Register the MCP Server

{
  "mcpServers": {
    "podcast_server": { "command": "/absolute/path/to/podcast_server/bin/run-mcp.sh" }
  }
}

Agents should read AGENTS.md and then your PLAYBOOK.md.


MCP Tools

Area

Tools

Getting oriented

podcast_capabilities

Catalog

podcast_status, podcast_list_episodes, podcast_get_episode, podcast_inspect

Publishing

podcast_stage_discovery, podcast_stage_from_descript, podcast_publish, podcast_update_episode, podcast_rebuild_feed, podcast_deploy_site, podcast_backup

Content

podcast_create_blog, podcast_draft_social, podcast_stage_thumbnail, podcast_render_thumbnail, podcast_render_episode_art, podcast_render_newsletter_preview, podcast_make_gif

Distribution

podcast_upload_media_asset, podcast_publish_to_youtube, podcast_set_youtube_thumbnail, podcast_post_to_x, podcast_post_to_instagram, podcast_crosspost_to_devto, podcast_crosspost_to_substack, podcast_draft_newsletter, podcast_pull_newsletter, podcast_send_newsletter, podcast_unschedule_newsletter

Content management

podcast_list_instagram_posts, podcast_delete_instagram_post, podcast_list_youtube_videos, podcast_delete_youtube_video, podcast_list_devto_articles, podcast_unpublish_devto_article, podcast_list_substack_posts, podcast_delete_substack_post

Metrics

podcast_metrics_collect, podcast_metrics_summary, podcast_episode_metrics, podcast_metrics_report

Common Commands

Task

Command

Check the setup

uv run python -m core.doctor

Rebuild feed (+ pages), no deploy

uv run python -m core.site

Import a back catalog

uv run python -m importers.rss <feed-url> --mirror

Back up

podcast_backup tool (runs in the background), or uv run python scripts/backup_to_hf.py

Collect metrics + write the dashboard

uv run python -m metrics.collect --report (schedule bin/collect-metrics.sh daily)

Connect or reconnect Instagram (access token)

uv run python -m exporters.instagram login

Connect or reconnect Substack (session cookie)

uv run python -m exporters.substack_post login

Run tests

uv run python -m unittest discover -s tests

Safety Guarantees

The engine refuses to:

  • Reuse a slug or regenerate a GUID, which would show duplicate episodes on every subscriber's device.

  • Re-add excluded episodes: a database trigger blocks them, even for raw SQL.

  • Publish unrelated website work: deploys commit only generated files, only from WEBSITE_BRANCH.

  • Publish broken downloads: episodes whose audio isn't in R2 are left out of the feed and pages.

  • Upload arbitrary files to the public bucket, or overwrite existing (immutably cached) objects.

  • Leak credentials: downloads are https-only, the Descript token is sent only to Descript hosts, the dev.to key only to dev.to, the Substack session cookie and the Instagram token stay in their own 0600 files (the doctor fails if others can read them), the Instagram token is sent only to Meta, in a header, and a user token pasted at login is swapped for the Page token and never stored, and X and YouTube tokens stay in their own CLIs' or OAuth files.

  • Publish cross-posts for you: dev.to and Substack copies are created as unpublished drafts (Substack never emails subscribers from the engine), and a blog post that already has a draft (logged, or found on the account) is never sent again.

  • Upload local files to Substack: Substack drafts accept only https images, because the library would otherwise upload any image path found on disk.

  • Attach arbitrary files: X media, Instagram images, and YouTube thumbnails accept only image/video types within each platform's size and shape limits, checked before any upload.

  • Post to the wrong Instagram account or twice: a token saved for another account than INSTAGRAM_USERNAME is refused, and the same image with the same caption returns the logged post instead of a second one.

  • Remove content in bulk or blindly: the delete and unpublish tools (Instagram, YouTube, dev.to, Substack) take one id per call, read the item first, refuse anything that isn't your account's own, and log what they removed. There is no delete-all tool.

  • Publish content nobody can find: episodes, blog posts, YouTube uploads, Instagram posts, and X media are checked against the content's discovery brief before anything goes public; skipping a check takes a discovery_override with a reason, which is recorded. YouTube titles, descriptions, and tags over the platform's limits are refused rather than silently cut.

  • Lose track of public actions: every X and Instagram post, YouTube upload, and thumbnail change is logged as soon as it succeeds, so an agent can check before retrying instead of posting twice.

Troubleshooting

Symptom

Cause and fix

git_deploy.status: skipped

The site repo isn't on WEBSITE_BRANCH. Switch branches and call podcast_deploy_site (see AGENTS.md §5).

git_deploy.status: failed, "could not read Username"

Git has no usable credentials. On headless Linux, keyring helpers (libsecret) can't unlock; use credential.helper store or a fine-grained token.

Rewritten show notes don't reach podcast apps

Edit them with podcast_update_episode (it clears stale imported HTML and rebuilds), then podcast_deploy_site.

Backup re-downloads audio every run

Catalog byte lengths don't match R2; sizes must come from the stored object (the importer's --mirror records them).

core.doctor: domain resolves publicly but not locally

Your resolver cached an old "no such host" answer; it clears on its own.

Doctor says a metrics source is ready, but collection fails

The doctor checks settings, not live access. The run's message says what to fix, e.g. Cloudflare does not have permission ... zone.analytics.read (add a token policy scoped to the zone/domain with Analytics Read) or YouTube API has not been used in project ... (enable YouTube Analytics API in that Cloud project).

podcast_crosspost_to_substack: "Substack sign-in failed"

The session cookie expired or you signed out of Substack. Run uv run python -m exporters.substack_post login with a fresh cookie value. Any other Substack error may mean its private API changed; check for a newer python-substack release.

podcast_crosspost_to_devto or _substack returns exists

A draft of that post is already there (logged, or found on the account). To make a fresh one, delete the old draft and remove the slug's line from $DATA_DIR/social/devto_posts.jsonl or substack_posts.jsonl.

podcast_post_to_instagram: "token is invalid, expired, or revoked"

The token expired or lost access (a password change or removed permission invalidates Page tokens; Unified login tokens follow their configuration's expiry). Get a new token as in ADOPTING and run uv run python -m exporters.instagram login.

podcast_post_to_instagram: container ERROR

Instagram couldn't fetch the staged image from R2_PUBLIC_DOMAIN; check the domain resolves publicly (core.doctor). Nothing was published, so retrying is safe.

"No discovery brief for ''" or "Discovery check failed"

Save the approved brief with podcast_stage_discovery, then fix the fields the message lists (e.g. put the primary phrase in the first 150 characters of the YouTube description). Nothing was published, so retrying is safe. Skip a check only with discovery_override="<the human's reason>".

Transcripts don't show in Apple Podcasts

In Podcasts Connect, set the show to display transcripts you provide; Apple reads only .vtt or .srt from podcast:transcript, so a .txt transcript serves other apps only.

YouTube upload result has captions or playlist failed

The video is live; only that step failed (often quota: a caption costs 400 units, a playlist entry 50). Add it by hand in YouTube Studio, or wait for the quota to reset. Don't re-upload.

podcast_set_youtube_thumbnail returns 403

Custom thumbnails aren't enabled for the channel; verify it in YouTube Studio (phone verification).


Project Structure

podcast_server/
├── core/                  # config, catalog (db), R2 storage, feed XML, site output + deploy, doctor and capabilities, discovery briefs and checks, episode transcript/chapters/art, thumbnail renderer, helpers
├── mcp_server/server.py   # the MCP tools, their annotations, and the connection instructions
├── importers/             # rss.py (any host, GUID-preserving), descript.py (staging)
├── exporters/             # Astro pages, blog posts, social drafts, X and Instagram posting, dev.to and Substack drafts, YouTube upload + thumbnails
├── metrics/               # collectors (OP3, Cloudflare, X, Instagram, YouTube, dev.to, Substack, plugins), store, analysis, dashboard (Chart.js in vendor/)
├── bin/                   # run-mcp.sh (MCP launcher), collect-metrics.sh (scheduler entry)
├── scripts/backup_to_hf.py
├── examples/show-data/    # template for your private DATA_DIR
├── tests/
├── pyproject.toml / uv.lock  # uv project: dependencies + lockfile
├── ADOPTING.md            # run your own show
├── AGENTS.md              # operating guide for agents
└── PLAN.md                # architecture and roadmap

License

Business Source License 1.1: free for personal, open-source, and internal business use; not for offering a competing commercial hosted podcast service. Converts to Apache 2.0 on October 26, 2029.


Built by Harpreet Sahota to run a real show, then made show-neutral so you can run yours.

X LinkedIn YouTube Website

Available Tools

12 tools
add_tagsAdd TagsB

Add tags to a specific draft/post.

NameRequiredDescriptionDefault
tagsYesstring or list of tag names (e.g. `"tech"` or `["tech", "python"]`).
draft_idYestarget draft identifier.

Output Schema

NameRequiredDescription

No output parameters

TDQS

B3.1/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description must carry the full behavioral burden and falls short. It does not state whether tags are appended or replace existing tags, how duplicates or unknown tag names are handled, or whether the operation requires any special permissions. The output schema covers return values, but the mutation semantics remain opaque.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single, front-loaded sentence with no wasted words. It is appropriately sized for the operation, though it could have added one clarifying phrase about append-vs-replace behavior at little cost.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a mutation tool with no annotations and no output schema explanation required, the description is too thin. It omits key behavior such as whether tags are additive, what happens to existing tags, and any constraints on tag names or duplicates, leaving an agent to guess at mutation semantics.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents both the draft_id and tags parameters, including that tags may be a string or list. The description adds no parameter-level detail beyond referencing the target, which is the expected baseline when the schema is complete.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a clear verb (add) and resource (tags) plus the target type (draft/post), so an agent can immediately tell what it does. It does not explicitly distinguish itself from siblings like put_draft, which could also modify a draft's content, but the tag-focused operation is clear enough.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is only implied: add tags to a draft. There is no explicit guidance on when to use this versus put_draft or other draft-modifying siblings, and no prerequisites or exclusions are mentioned.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_draftGet DraftC

Get a specific draft by its ID.

NameRequiredDescriptionDefault
draft_idYesThe identifier of the draft.

Output Schema

NameRequiredDescription

No output parameters

TDQS

C2.9/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full behavioral burden, yet it says nothing about it being a read-only lookup, permission requirements, or what happens when the ID does not exist. An agent can infer "read" from the verb, but nothing about failure modes or access constraints is disclosed.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single short sentence with the key qualifier ("by its ID") front-loaded and no filler. There is nothing to trim, though also little substance.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The tool is simple: one required parameter with full schema coverage and an output schema that documents return values, so most needs are covered structurally. The missing piece is any note on behavior for an unknown draft ID or on how this fits alongside list_drafts.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% — draft_id is typed as an integer and documented — so the baseline of 3 applies. The description adds only that the ID identifies a "specific draft," offering no format or scoping detail beyond the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource ("Get a specific draft") and adds the retrieval key ("by its ID"), which implicitly distinguishes it from the list_drafts sibling. It stops short of naming any sibling explicitly, so it does not fully earn a 5.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no guidance on when to use this versus list_drafts or the various draft-mutation siblings (put_draft, prepublish_draft, publish_draft). The only hint is "by its ID," implying the caller already has an identifier, which is implied usage at best.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_statusGet StatusA

Get the authentication status and basic user information.

NameRequiredDescriptionDefault

No parameters

Output Schema

NameRequiredDescription

No output parameters

TDQS

A3.6/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description must carry the behavioral burden, and it does say the tool reads auth state plus basic user info. It stops short of disclosing important traits for a status tool — whether it errors when unauthenticated versus returning an unauthenticated status, and whether it is a cheap connectivity ping or a heavier lookup.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single front-loaded sentence with no filler or redundancy. Nothing in it fails to earn its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return values need not be explained, and zero-parameter tools have a low bar. Even so, the description omits the one behavior that matters for a status tool — what happens when the caller is not authenticated — leaving it only minimally complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes zero parameters, so there is no parameter semantics to document. The baseline of 4 applies; nothing in the description is needed or missing on this dimension.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb ('Get') and a specific resource ('authentication status and basic user information'), so the agent knows exactly what it returns. It is clearly distinct from all siblings (draft/publication tools), though the description doesn't explicitly say so.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is only implied — an agent can infer this is a session/credential check — but there is no explicit 'use this before X' or 'use this to verify Y' guidance, and no mention of alternatives. For a zero-parameter utility tool this is acceptable, but it leaves the agent to reason about when to call it.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_draftsList DraftsC

List drafts for the current publication.

NameRequiredDescriptionDefault
limitNoMax number of drafts to return.
filterNoFilter string, defaults to "draft".draft
offsetNoPagination offset.

Output Schema

NameRequiredDescription
resultYes

TDQS

C2.9/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full behavioral burden. It does not mention read-only/safety traits, pagination behavior, permissions, or rate limits; it only adds the 'current publication' scope.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single front-loaded sentence with no filler. It is appropriately concise, though its brevity contributes to gaps elsewhere.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The tool is simple, has an output schema, and has complete parameter schema coverage, so the description needn't explain return values or parameter details. However, it still lacks enough usage and behavioral context for an agent to confidently choose it over sibling draft tools.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already explains limit, filter, and offset. The description adds no parameter semantics beyond the schema, which matches the baseline for fully documented parameters.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (List) and resource (drafts) and scopes it to the current publication. It does not explicitly distinguish itself from nearby tools like get_draft, but the basic purpose is clear.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives no explicit when-to-use guidance, no exclusions, and no alternatives. An agent must infer that this is the bulk-listing tool versus get_draft or list_publications.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_publicationsList PublicationsB

List all publications available to the authenticated user.

NameRequiredDescriptionDefault

No parameters

Output Schema

NameRequiredDescription
resultYes

TDQS

B3.4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden. It does disclose the access scope ('available to the authenticated user'), implying results are limited to the caller and that auth is required, and 'list' implies a read-only operation. It says nothing about pagination, ordering, or result limits, which is a meaningful gap for a collection endpoint.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single front-loaded sentence with no filler; the scope constraint appears immediately after the verb and resource. Nothing could be removed without losing information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The presence of an output schema means return values need not be explained, and with zero parameters the schema burden is minimal. However, the absence of annotations leaves the description responsible for safety and behavior disclosure, and it omits pagination, ordering, and any exclusivity versus list_drafts, so it is only minimally viable.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes zero parameters, so there is nothing for the description to disambiguate; baseline is 4. No conflicting parameter hints are present.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb ('List') and resource ('publications') with a scope qualifier ('available to the authenticated user'), which is more precise than a bare name restatement. It does not explicitly distinguish itself from the sibling list_drafts, but the resource noun makes the difference inferable.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No statement of when to use this instead of list_drafts or the draft-related siblings, and no stated prerequisites beyond a vague reference to being authenticated. The agent must infer the use case entirely from the tool name.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

post_draft_from_markdownPost Draft From MarkdownA

Create or update a Substack draft from Markdown.

This tool builds a Substack `Post` from markdown content and posts a draft.
It supports optional tag assignment, prepublish (setup check), and publishing.

Args:
    title: Draft title.
    markdown: Markdown body content.
    subtitle: Optional subtitle text.
    audience: One of `everyone`, `only_paid`, `founding`, `only_free`.
    write_comment_permissions: One of `none`, `only_paid`, `everyone`.
    search_engine_title: Optional title for search engine optimization.
    search_engine_description: Optional description for search engine optimization.
    slug: Optional URL slug for the post.
    draft_section_id: Optional section ID for the draft.
    tags: Tag or list of tags to attach to the post.
    prepublish: If true, calls `prepublish_draft` after creation.
    publish: If true, calls `publish_draft` after creation (and optionally prepublish).
    send: Passed to `publish_draft` for newsletter delivery.
    share_automatically: Passed to `publish_draft`.

Returns:
    dict containing drafted post (`draft`), optional `tags`, `prepublish`, `publish` results.

Examples:
    With the YAML structure from the README, a caller can map fields like:

    ```yaml
    title: "My Post Title"
    subtitle: "My Post Subtitle"
    audience: "everyone"
    write_comment_permissions: "everyone"
    markdown: |
      # Hello

      This is the body.

    tags:
      - python
      - substack
    prepublish: true
    publish: true
    send: false
    share_automatically: true
    ```

    Then invoke via MCP directly:

    ```python
    from substack_mcp.mcp_server import post_draft_from_markdown

    result = await post_draft_from_markdown(
        title='My Post Title',
        markdown='# Hello

This is the body.', subtitle='My Post Subtitle', audience='everyone', write_comment_permissions='everyone', tags=['python', 'substack'], prepublish=True, publish=False, # set true when ready ) print(result) ```

    A longer process with manual prepublish/publish calls:

    ```python
    from substack_mcp.mcp_server import (
        post_draft_from_markdown,
        prepublish_draft,
        publish_draft,
        add_tags,
    )

    d = await post_draft_from_markdown(
        title='Long flow',
        markdown='Content',
        tags=['a','b'],
        publish=False,
    )
    draft_id = d['draft']['id']

    await add_tags(draft_id, ['post-tag', 'news'])
    await prepublish_draft(draft_id)
    await publish_draft(draft_id, send=True, share_automatically=True)
    ```

    This docstring example is meant to mirror the YAML-driven workflow and show how to decompose the same operations into explicit tool calls.
NameRequiredDescriptionDefault
sendNo
slugNo
tagsNo
titleYes
publishNo
audienceNoeveryone
markdownYes
subtitleNo
prepublishNo
draft_section_idNo
search_engine_titleNo
share_automaticallyNo
search_engine_descriptionNo
write_comment_permissionsNoeveryone

Output Schema

NameRequiredDescription

No output parameters

TDQS

A3.6/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations at all, the description carries the full burden, and it does disclose meaningful behavior: chained side effects when prepublish/publish are true, newsletter delivery via send, and the shape of the return dict. It omits the highest-stakes facts — that publish=true makes the post publicly live, that send=true (the schema default!) emails subscribers, and any auth prerequisite. The 'create or update' claim is also unexplained since no identifier parameter controls which mode applies.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The first sentence is well front-loaded and the Args block is scannable, but the description is bloated: a YAML block plus two full Python examples, a Returns section redundant with the output schema, and a meta-sentence explaining what the examples are for. Roughly half the text restates information an agent already has from the schema.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 14-parameter mutation tool with 0% schema coverage, the description covers most of what an agent needs: parameter listing, enums, chaining semantics, and return shape (redundant given the output schema). The remaining gaps are the create-vs-update mechanism and the risk profile of publish/send, which matter for a write tool with no annotations.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0% and there are 14 parameters, so the description must compensate — and it does list every one, including enum values for audience and write_comment_permissions that the schema lacks. But several entries are near-tautological ('subtitle: Optional subtitle text') and send/share_automatically are dismissed as 'Passed to publish_draft' with no explanation, and no defaults are stated despite the surprising send=true default.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening sentence gives a specific verb and resource ('Create or update a Substack draft from Markdown') and the body clarifies it builds a Post and posts a draft. It also names the sibling tools it can chain into (prepublish_draft, publish_draft), which helps distinguish it from those. It does not, however, differentiate itself from put_draft, the other draft-writing sibling.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The two worked examples implicitly establish when to use the combined call versus decomposing into add_tags/prepublish_draft/publish_draft manually, and the closing sentence states that intent explicitly. What is missing is a stated rule for choosing between the two approaches and any precondition ('the draft must not already be published').

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

prepublish_draftPrepublish DraftC

Invoke prepublish checks for a draft.

NameRequiredDescriptionDefault
draft_idYestarget draft identifier.

Output Schema

NameRequiredDescription

No output parameters

TDQS

C2.7/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden and does not say whether invoking checks mutates the draft, whether it is idempotent, whether failures block publication, or what permissions are needed. It only restates the action.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single front-loaded sentence with no filler, though the terseness comes at the cost of explaining 'prepublish checks'.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The output schema covers return values and the single parameter is documented, but for a tool sitting among several publish variants, the description should clarify how it differs from publish_draft_checked and whether the checks are read-only.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% for the single draft_id parameter, so the schema already documents the argument. The description adds no semantics about what draft identifiers are valid or how the checks relate to that draft beyond the name, so the baseline of 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose3/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description names an action ('invoke prepublish checks') on a resource ('draft'), which is more than a tautology, but 'prepublish checks' is unexplained jargon and nothing distinguishes it from siblings like publish_draft_checked or publish_draft, which plausibly also perform validation.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no guidance on when to call this versus publish_draft, publish_draft_checked, or get_status. Given the dense cluster of publish-related siblings, the absence of any exclusion or sequencing hint leaves the agent to guess.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

publish_draftPublish DraftA

Publish a draft to live post state. (Legacy compatibility interface).

This tool remains for backward compatibility. It is recommended to use publish_draft_checked instead, which provides a safer publishing path with explicit confirmation and prepublish validation.

NameRequiredDescriptionDefault
sendNoif False then do not send email to subscribers.
draft_idYestarget draft identifier.
share_automaticallyNowhether to auto-share (e.g. social propagation).

Output Schema

NameRequiredDescription

No output parameters

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the behavioral load: it discloses that this path publishes to a live state and lacks the confirmation and prepublish validation of the checked variant, which is a meaningful risk disclosure. It stops short of saying whether publishing is reversible or what side effects occur (e.g. default email send), though the output schema covers return shape.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The purpose and the legacy routing are front-loaded and the text is short. The parenthetical "(Legacy compatibility interface)" is then restated as "This tool remains for backward compatibility", a small redundancy that keeps it from a 5.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists and parameters are fully documented, so the description only needs to cover purpose, routing and risk. It does those, but leaves out what actually changes on the draft and whether the action is reversible — the one thing an agent deciding to call a publishing mutation would want.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so all three parameters (send, draft_id, share_automatically) are already documented in the schema with defaults. The description adds no parameter-level meaning beyond that, so baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The first sentence gives a specific verb and target state ("Publish a draft to live post state"), which is precisely distinguishable from siblings like prepublish_draft and schedule_draft. It also names the near-duplicate sibling publish_draft_checked so the agent can tell the two apart without opening either schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly states this tool exists only for backward compatibility and recommends publish_draft_checked instead, giving the reason (explicit confirmation and prepublish validation). That is a clear when-to-prefer-the-alternative rule, which is exactly what usage guidance should do.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

publish_draft_checkedPublish Draft CheckedB

A safer publishing path that requires confirmation and runs prepublish checks.

NameRequiredDescriptionDefault
sendNoif False then do not send email to subscribers. Defaults to False.
confirmNoMust be True to proceed with publication.
draft_idYestarget draft identifier.
share_automaticallyNowhether to auto-share.

Output Schema

NameRequiredDescription

No output parameters

TDQS

B3.4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full behavioral burden. It usefully discloses that confirmation is required and prepublish checks are run, but it does not explain side effects, irreversibility, permissions, or what happens if checks fail.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The definition is a single front-loaded sentence with no wasted language. It is appropriately concise, though its brevity leaves important selection and safety details to other fields.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The tool has a rich schema with full parameter coverage and an output schema, so return values need not be described. However, for a publishing mutation with no annotations, the description should do more to clarify how it differs from sibling publish/prepublish tools and what the checks imply.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so all four parameters are already documented in the schema. The description adds no parameter-specific meaning beyond the existing schema text, making 3 the appropriate baseline.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific action and resource: publishing a draft through a safer path with confirmation and prepublish checks. It partially distinguishes itself from siblings like publish_draft and prepublish_draft by calling out the safer/checked workflow, though it does not explicitly compare them.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The phrase 'A safer publishing path' implies when to choose this over a normal publish operation, but the description never names alternatives or states when not to use it. Usage is inferable rather than explicit.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

put_draftPut DraftC

Update an existing draft by draft ID.

NameRequiredDescriptionDefault
draft_idYestarget draft identifier.
update_payloadYesdict of fields supported by Substack `put_draft` (e.g. `slug`, `draft_section_id`).

Output Schema

NameRequiredDescription

No output parameters

TDQS

C2.9/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden, yet it says nothing about whether the update is a full replacement or a merge, what permissions are required, whether changes are reversible, or what happens to omitted fields. 'Update an existing draft' implies mutation and a precondition (draft must exist), but that is the extent of the disclosure.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single front-loaded sentence with zero waste. The brevity edges into under-specification rather than verbosity, but structurally nothing is misplaced.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

This is a mutation tool with no annotations, a nested free-form update_payload, and only an output schema to lean on. The description should clarify which fields are updatable and what the payload actually does, but it offers nothing beyond the schema's example fields.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so both parameters are already documented in the schema, and the description adds no syntax, format, or constraint detail beyond it. Baseline 3 applies when the schema does all the work.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb ('update') and resource ('existing draft by draft ID'), which is clear enough on its own. It does not differentiate from siblings like post_draft_from_markdown or prepublish_draft that also touch drafts, so an agent still has to infer where the boundary lies.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives no conditions for use, no exclusions, and no alternatives. It never says whether this should be used for content edits versus publishing/scheduling flows covered by publish_draft or schedule_draft, leaving routing entirely to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

schedule_draftSchedule DraftC

Schedule a draft for release.

NameRequiredDescriptionDefault
atYesISO 8601 formatted datetime string (e.g., "2024-01-01T12:00:00Z").
draft_idYestarget draft identifier.

Output Schema

NameRequiredDescription

No output parameters

TDQS

C2.6/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden, yet it discloses nothing about side effects: whether the draft auto-publishes at the given time, what permissions are required, whether scheduling an already-scheduled draft overwrites or errors, or whether the time must be in the future. For a mutation tool this is a substantial gap.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single front-loaded sentence with no filler, which is efficient. Its brevity reflects under-specification more than careful economy, but on this dimension alone it is well-formed.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Although an output schema exists and parameters are fully documented, the description leaves the core behavior of a scheduling mutation unexplained (auto-publish semantics, preconditions, interaction with prepublish_draft). An agent could invoke it but could not predict its consequences.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% — both draft_id and the ISO 8601 format of `at` are documented in the schema — so the baseline of 3 applies. The description adds no meaning beyond it, notably omitting whether `at` must be in the future or which timezone assumptions apply.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose3/5

Does the description clearly state what the tool does and how it differs from similar tools?

The verb-and-resource pair ("schedule" + "draft") is identifiable, but "for release" is ambiguous — it does not say whether this queues automatic publishing, marks a prepublish date, or merely records intent. It also does not distinguish itself from close siblings such as publish_draft, prepublish_draft, and unschedule_draft.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance on when to schedule rather than publish_draft or prepublish_draft, and no mention of unschedule_draft as the inverse operation. An agent must infer the workflow entirely from sibling names.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

unschedule_draftUnschedule DraftC

Unschedule a previously scheduled draft.

NameRequiredDescriptionDefault
draft_idYestarget draft identifier.

Output Schema

NameRequiredDescription

No output parameters

TDQS

C2.9/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden of behavioral disclosure. It identifies the operation as an unschedule mutation and implies a precondition, but says nothing about permissions, reversibility, error behavior if the draft is not scheduled, or what state the draft returns to.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single front-loaded sentence with no unnecessary words. It is appropriately sized for a simple one-parameter tool, though its brevity leaves important behavioral details unstated.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return values need not be explained. For a simple mutation tool with one well-documented parameter, the description is minimally adequate, but it omits behavioral context about post-conditions and failure modes that would matter when unscheduling a draft.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema has 100% description coverage and documents the single draft_id parameter. The description does not add any parameter-level meaning beyond what the schema already provides, so the baseline of 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource ('Unschedule ... draft') and adds the precondition that the draft was previously scheduled. It is clear, but it does not explicitly distinguish itself from the sibling schedule_draft beyond the antonym in the verb.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no explicit guidance on when to use this tool versus alternatives such as schedule_draft or put_draft. The phrase 'previously scheduled' implies a precondition, but no when-to-use or when-not-to-use rules are stated.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 12 tool updatesv0.1.0
    • First observedadd_tags
    • First observedget_draft
    • First observedget_status
    • First observedlist_drafts
    • First observedlist_publications
    • First observedpost_draft_from_markdown
    • First observedprepublish_draft
    • First observedpublish_draft
    • First observedpublish_draft_checked
    • First observedput_draft
    • First observedschedule_draft
    • First observedunschedule_draft

TDQS

B3.1/5.0

Scored across 12 tools

Disambiguation3/5

Several tools overlap: post_draft_from_markdown bundles create/update plus prepublish/publish/tags, duplicating add_tags, prepublish_draft, and publish_draft, and put_draft also updates drafts, muddying create-vs-update boundaries. publish_draft and publish_draft_checked are near-duplicates, though the descriptions explicitly flag the legacy/safer distinction. Descriptions help but an agent must still reason about which of several paths to use.

Naming Consistency4/5

Most tools follow a clean verb_noun pattern (add_tags, prepublish_draft, publish_draft, get_status, list_drafts, get_draft, schedule_draft, unschedule_draft). Minor deviations: put_draft uses 'put' instead of 'update', and post_draft_from_markdown is a longer, less uniform form. Overall consistent and readable.

Tool Count4/5

Twelve tools is well-scoped for a publishing workflow and each maps to a recognizable operation. Slight redundancy from the legacy publish_draft alongside publish_draft_checked, but count is reasonable.

Completeness3/5

Draft lifecycle is mostly covered (create, get, list, update, publish, schedule/unschedule, add tags), but there is no delete_draft and no way to remove tags. Publication listing exists without a corresponding get/select-publication operation, leaving a few dead ends.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables deployment of autonomous AI agents with memory and tool execution capabilities through a WebSocket-based MCP protocol. Provides production-ready infrastructure with REST API access, persistent state management, and extensible function registry for building self-hosted AI systems.
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    MCP server for self-hosted static site publishing on Cloudflare Workers. Enables AI coding agents to deploy pages with a single 'publish' tool and get live URLs, with support for atomic updates, versioning, and per-site passwords.
    2 npm
    2
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables self-hosted AI gateway and agent control plane with governed MCP tools, virtual-key budgets, caching, and audit, supporting OpenAI, Anthropic, Gemini, MCP, and A2A protocols. It provides deterministic prompt enhancement and governed execution with a zero-credential first run.
    10
    Apache 2.0
  • A
    license
    Not graded
    quality
    B
    maintenance
    A hosted MCP server that lets AI assistants schedule and publish social media posts across Instagram, TikTok, X, LinkedIn, YouTube, Pinterest, Facebook, Telegram, Threads, and Bluesky. It provides tools for managing accounts, posts, media, and analytics, including post previewing before publishing.
    1
    MIT