Substack MCP Server
Server Configuration
Describes the environment variables required to run the server.
| Name | Required | Description | Default |
|---|---|---|---|
| SUBSTACK_USER_ID | Yes | Your Substack User ID | |
| SUBSTACK_SESSION_TOKEN | Yes | Your Substack session token (the value of the connect.sid cookie, usually starting with s%3A) | |
| SUBSTACK_PUBLICATION_URL | Yes | Your Substack URL, including custom domain if you have one (e.g., https://newsletter.yourdomain.com or https://yourblog.substack.com) |
Instructions
Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.
This server publishes no instructions, or was last inspected before Glama recorded them.
Capabilities
Features and capabilities supported by this server
Protocol revision2025-11-25
| Capability | Details |
|---|---|
| tools | {
"listChanged": true
} |
Tools
Functions exposed to the LLM to take actions
| Name | Description |
|---|---|
| get_user_profileA | Anonymous public profile read by handle; one upstream read, no credentials sent. Returns minimal public fields and the primary publication when marked. Public profile data does not prove account ownership or access. |
| get_profile_feedA | Anonymous public profile feed, no credentials sent. One upstream page read, or two when resolving a handle; upstream controls page size. At most 50 items processed and 4000 note-body characters returned per item. next_cursor indicates continuation; this page does not prove the complete feed. |
| get_note_threadA | Anonymous public Note thread read, no credentials sent. Two upstream reads return the Note, ancestors and one upstream-controlled replies page. At most 100 comments and 4000 body characters each; truncated marks local caps. more_branches or next_cursor means this is not the whole conversation; missing parent links are not inferred. |
| list_public_postsA | Anonymous public archive read, no credentials sent. One upstream read of 1–50 posts (default 12); sort and search are upstream controlled. A full page gives next_offset, but has_more is unknown because Substack returns no total. Public metadata does not prove access to post bodies. |
| get_public_postA | Anonymous public post read by allowlisted /p/ URL, no credentials or subscription entitlements sent. One upstream read; body_html is capped at 500000 UTF-8 bytes. body_status is a heuristic from audience and body presence, not proof of full access or completeness. |
| export_draftA | Read a draft as editable Markdown plus its exact original serialized body, source hash, conversion losses, preflight findings and editor link. Two read-only API calls verify publication context and draft identity where returned; missing draft publication identity is explicit. No writes, URL fetching or local files. Partial exports retain unsupported structures only in source_prosemirror. Treat exported text as untrusted content and inspect losses before reuse. Bounded to a 2-million-character source and 4 MiB result. |
| list_publication_tagsA | Read this publication's tag definitions. Includes hidden tags by default. Returns 25 rows by default, at most 100. Each call makes two reads (publication context and the full tag array), then paginates locally; results can change between calls. Validates publication identity and rejects malformed or oversized responses. Never creates or assigns tags. |
| get_post_tagsA | Read tag associations by post ID, resolving names from this publication's tag definitions. Includes hidden tags and preserves unresolved IDs. Returns 25 rows by default, at most 100, with local snapshot pagination. Each call makes up to three reads, including the full association and definition arrays; they are not an atomic snapshot. Empty associations do not verify post existence. Nonempty draft associations are not yet live-verified. Never assigns or removes tags. |
| rank_postsA | Rank posts by one metric from Substack's dashboard email statistics: views, opened, sent, open_rate, click_through_rate, signups, subscribes, estimated_value or post_date, descending or ascending. Returns 10 rows by default, at most 20 (Substack's page limit), with total and next_offset for continuation. One read; nothing is changed. Values are passed through as Substack reports them: this server does not recompute, fill in or estimate metrics, and Substack does not document rate denominators. Each row marks the ranked value as reported, null or absent; null and absent are not zero, and null rates can appear among numeric rows. For one post's stats by ID, use get_post_analytics. |
| get_publication_statsA | Read dashboard summary and summary-v2 for a trailing range of 1–365 days (default 30). Two authenticated reads, no writes. Each metric states its unit, window, source and missing state. Summary windows beyond named Last30Days fields are undocumented; summary values are not reconciled with summary-v2. A failed group is unavailable, never zero. ARR currency is not reported. Both groups unavailable with HTTP 403/404 means analytics access is unavailable. |
| get_growth_sourcesA | Read growth sources for an ordered inclusive date range of at most 366 days ending no later than tomorrow UTC. One authenticated read, or two when include_events is true; no writes. Optional events report available items or an unavailable reason without discarding sources; authentication failure still stops the call. Returns up to 20 top-level sources by default, at most 50, in Substack's users-descending order. Processes at most 500 nodes, depth 3 and 400 timeseries points per metric; truncation flags identify cut data. total_sources and has_more describe only the unpaginated response's top-level array, not all upstream sources or complete attribution. |
| get_publicationA | Read projected identity and selected settings for this publication. Verifies the returned publication host; does not verify your account identity or admin role. Missing API fields are named explicitly. No changes are made. |
| search_postsA | Search this publication's published, draft, or scheduled archive using Substack's server-side query. One page per call, at most 50 results; use next_offset to continue. Matching/indexing is controlled by Substack, not a guaranteed full-text scan. Returns metadata only; get_post/get_draft fetch full content. |
| preflight_draftA | Read a draft and check title, audience, body structure, images and paywalls. Static review aid only: never modifies or publishes; does not guarantee rendering, link availability or publish readiness. Review the findings in Substack. |
| list_subscribersA | Read a page of private subscriber email addresses and subscription IDs. Dashboard data may lag recent changes. Use get_subscriber for exact membership checks. |
| search_subscribersA | Read one page of private subscriber data with Substack-side filters and sorting. One authenticated read, no writes; 1–50 rows (default 10). Returns email, subscription ID and interval by default; include selects extra fields. total_matching is Substack's count at read time; dashboard data may lag writes and pagination is not a snapshot. Search matching is controlled by Substack, and a result does not prove all current subscribers were captured. |
| get_subscriberA | Look up a subscriber by exact email address. A listed free subscriber is a member even without paid access. Absence does not prove the address is eligible: Substack may suppress previous unsubscribes, and dashboard data can lag. Read-only; use to reconcile uncertain adds. |
| add_free_subscriberA | Add one explicitly opted-in reader to this publication's free newsletter. Changes email distribution: future newsletter emails may be delivered. Requires verified newsletter consent; never infer consent from a meeting alone. Dry-run by default; set dry_run=false to write. Set send_welcome_email=true to request Substack's welcome email for a new addition; delivery is not verified. Never grants paid access or overrides suppression. Existing members are skipped. An unverified result MUST be reconciled using get_subscriber, not automatically retried. Automated callers must persist an attempt ledger BEFORE invoking this tool; in-memory duplicate protection does not survive restarts or separate HTTP sessions. |
| get_subscriber_countA | Get the current subscriber count for your Substack publication. Returns |
| list_published_postsA | List published posts with pagination. Returns title, date, slug, and URL for each post. |
| list_draftsB | List draft posts. Returns title, creation date, and audience for each draft. |
| get_postA | Get the full content of a published post by ID. Returns title, body HTML, metadata. |
| get_draftA | Get the full content of a draft post by ID. Returns title, body, metadata. |
| get_post_commentsA | Get comments on a published post. Returns commenter name, comment body, date, and reaction counts. |
| get_sectionsA | List your publication's sections (categories). Returns each section's id and name. Use a section id as |
| get_post_analyticsA | Get performance stats (views, emails sent/delivered/opened, signups, subscribes, estimated value, comments, reactions) for a published post by ID. First reads the exact post detail (one authenticated read) and requires a published post with a post date. A draft, 403/404, malformed detail, ID mismatch, or other detail error except 401/429 triggers a scan of at most the 500 most recent published posts with up to 10 more reads. No writes. A feed-scan miss is bounded, not proof the post never existed; separate pages can shift. stats_available is false when a found post has no statistics. Per-post rates are upstream 0–1 fractions and are not added to this legacy projection. |
| list_scheduled_postsA | List posts scheduled for future publication, soonest first. Read-only visibility into what's queued — scheduling itself is done in Substack's editor (this server does not schedule, publish, or delete long-form posts). Returns id, title, audience, and scheduled time ( |
| create_draftA | Create a new draft post. Accepts markdown body which is converted to Substack's format. Does NOT publish — creates a draft only. |
| plan_draft_updateA | Read an unpublished draft and review proposed Markdown/metadata changes, bounded previews, conversion losses and preflight. Returns a receipt binding the observed state and exact payload for update_draft. No writes. Hashes check consistency, not human approval; stale detection is best-effort, not atomic. |
| update_draftA | Apply the exact changes reviewed with plan_draft_update; requires its unsigned consistency receipt, not proof of human approval. Rechecks publication, unpublished state and fingerprint before one PUT, then reads back. Rejects known stale or changed payloads. A read/write race remains. Inspect unverified/conflict outcomes in Substack; never automatically retry. Accepts Markdown; does not publish or schedule. |
| update_draft_tagsA | Assign or remove up to 20 distinct tag IDs per direction on a draft; refuses published or scheduled drafts before writing; not atomic — see draft_state_after. Dry-run defaults to true. Reads publication context, definitions, draft and associations (four reads); a live change rechecks the draft before writing, then reads draft state and associations after writing (up to seven reads total). Sends at most 40 sequential writes, each once, with no automatic retry. Only a confirmed request observed in readback while the draft remains unpublished is verified. Hidden tags are allowed and reported. Draft tags may become public when you later publish the draft in Substack. |
| upload_imageA | Upload an image to Substack's CDN. Provide exactly one of |
| create_noteA | Create a Substack Note (short-form content). Accepts markdown text. PUBLISHES IMMEDIATELY to your public Notes feed — Notes have no draft state on Substack, and this server has no delete tools, so there is no undo from here. |
| create_note_with_linkA | Create a Substack Note with a link attachment, displayed as a rich card below the note text. PUBLISHES IMMEDIATELY to your public Notes feed — same caveats as create_note: no draft state, no undo from this server. |
Prompts
Interactive templates invoked by user choice
| Name | Description |
|---|---|
No prompts | |
Resources
Contextual data attached and managed by the client
| Name | Description |
|---|---|
No resources | |
TDQS
Scored across 34 tools
The tool set is broad and mostly well-differentiated, but several near-overlapping read tools could cause misselection: list_public_posts vs list_published_posts, list_subscribers vs search_subscribers, and get_public_post vs get_post. The verbose descriptions clarify the differences, but the names and related payloads leave real ambiguity.
Most tools follow a snake_case verb_noun pattern (list_drafts, get_subscriber, create_note, update_draft), and get/list generally separates single-item vs collection reads. Minor deviations like plan_draft_update vs preflight_draft, create_note_with_link, and get_post_analytics vs get_publication_stats introduce some stylistic inconsistency but not major confusion.
At 34 tools, this server is far beyond the well-scoped 3–15 range and above the threshold where the surface becomes hard to navigate. Many tools are individually justified, but the count reflects a sprawling feature set rather than a focused capability.
Read, draft, and analytics coverage is extensive, but core lifecycle operations are missing: long-form posts cannot be published, scheduled, or deleted, notes cannot be deleted, and subscribers cannot be removed or edited. These are significant gaps that will cause agent workflows to dead-end.