MCP server for Substack — create, update, schedule, publish posts with images and cover thumbnails
Substack MCP has well-structured tool definitions with clear naming (verb_noun pattern), comprehensive descriptions (avg 180 chars), and complete input schemas with types and enums. All 11 tools are explicitly registered with @mcp.tool() decorators. However, output schemas are not formally documented in the code, only described in docstrings. Parameter descriptions are solid but some lack format/constraint details (e.g., iso_datetime format not validated in description). Error handling is minimal, no recovery guidance or actionable error messages visible. Security is reasonable (no secrets in params), but no permission gates or audit logging. Composition is clean: each tool has one responsibility, and tool chains are well-supported (e.g., upload_image → set_cover_image). Missing: dry-run/confirmation for irreversible ops (publish_draft, delete_draft, post_note), and structured error responses.
Create a new Substack draft post from Markdown. Args: title: Post title (max 280 chars). content_markdown: Body in Markdown. Supports headings, bold/italic, links, bullet lists, blockquotes, code blocks, and images ( - local paths are auto-uploaded to Substack CDN). subtitle: Optional subtitle (max 280 chars). audience: Who can read it: 'everyone' (default), 'only_paid', 'founding', or 'only_free'. Returns: Summary including post_id, title, edit_url.
Permanently delete a draft. This cannot be undone. Args: post_id: Draft ID. Returns: {post_id, deleted: true}
Get full details of a specific draft, including the body content. Args: post_id: Draft ID. Returns: Full draft data including draft_body (ProseMirror JSON).
List recent drafts (unpublished posts). Args: limit: Max number of drafts to return (1-50). Default 10. Returns: List of draft summaries with post_id, title, edit_url, etc.
Post a Note (Substack's short-form, X/Threads-like post) to the public feed. Notes are different from Posts: - No title or subtitle. - No email delivery to subscribers. - Not added to the publication's article archive. - Visible in the Substack Notes feed (cross-publication discovery surface). Use Notes for: - Quick thoughts, links, restacks, questions to your audience - Daily presence between long-form posts - Networking with other Substackers (replies, mutual follows) Args: text: Plain text body. Use \n\n to separate paragraphs and \n for soft line breaks. Max 4000 chars (Substack's practical Notes limit). Returns: {note_id, url, raw}
Output schemas not formally documented. Docstrings describe return values (e.g., '{post_id, title, edit_url}') but no JSON Schema definitions visible in code. LLMs cannot reliably parse unstructured return descriptions.
Irreversible operations (publish_draft, delete_draft, post_note) lack confirmation/dry-run support. Agents can accidentally publish or delete without a safety gate.
Error handling is minimal. No recovery guidance in code (e.g., 'Try list_drafts() first if post_id is unknown'). Errors will not guide LLM to self-correct.
Inferred effective spec: 2025-06-18+.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | C | 61 | 2025-06-18+ | v2 |
Publish a draft immediately. Args: post_id: Draft ID. send_email: If True (default), send the post as an email to subscribers. If False, publish to web only without emailing. share_automatically: If True, auto-share to Substack social channels. Returns: {post_id, title, public_url, post_date, send_email}
Schedule a draft to publish at a future datetime. Args: post_id: Draft ID. iso_datetime: ISO 8601 datetime, e.g., '2026-05-15T09:00:00+09:00' for JST or '2026-05-15T00:00:00Z' for UTC. Returns: {post_id, scheduled_for}
Set the cover (thumbnail) image for a draft. The cover image is shown on the publication homepage, in social shares, and as the email header. Use upload_image first to get a CDN URL. Args: post_id: Draft ID. image_url: Substack CDN URL from upload_image, or any public image URL. Returns: Updated draft summary.
Cancel a scheduled publish, keeping the post as a draft. Args: post_id: Draft ID. Returns: {post_id}
Update an existing draft. Provide only the fields you want to change. Args: post_id: Draft ID returned by create_draft or list_drafts. title: New title (optional). subtitle: New subtitle (optional). content_markdown: New body in Markdown (optional, replaces full body). audience: New audience setting (optional). Returns: Updated draft summary.
Upload an image to Substack's CDN and return its public URL. Args: image_path: Local file path (e.g., /Users/foo/cover.png) or remote URL. Returns: {url, id, width, height} — pass `url` to set_cover_image or embed in Markdown as .
Parameter format constraints not enforced in descriptions. iso_datetime in schedule_draft lacks validation hint; image_path in upload_image does not specify supported formats or size limits.
No pagination support in list_drafts. Limit defaults to 10 but no offset/cursor or total_count returned. Large result sets could exhaust context.