substack-mcp

MCP serverDatabases & data

This connects your AI to your Substack publication so it can write for you. Long-form posts are always saved as drafts, so nothing goes live until you review and publish it yourself. Short-form Notes can be published to Substack right away.

Unavailable. This server has no hosted endpoint yet, so Ahel can't serve it.

Add to setup to save this item as a reference. Ahel cannot run it, and signing in will not install it.

What your AI can do with it

  • Create long-form Substack posts as drafts for your review
  • Publish short-form Notes to Substack immediately
  • Keep every post in draft form until you decide to publish
  • Share quick updates with your readers through Notes

Getting started

  1. Save this item in Your setup as a reference.
  2. Read the source or reference documentation for its setup requirements. Saving it here does not connect it to your AI.
  3. Check this page for availability before trying to install it through Ahel.

From the project's README

As published by conorbronsdon/substack-mcp in README.md.

Create and manage your Substack newsletter from your AI assistant or terminal. Prepare rich drafts, search your archive, publish Notes, inspect analytics, and manage explicitly consented free subscribers across publications. Review and publish long-form posts in Substack.


The demo runs actual MCP handlers against offline sample data. No live API calls or publication occur. Follow the draft workflow to create, find, export and review a post.

Safe by design — with one loud exception: This server cannot publish or delete long-form posts. Post tools create and edit drafts only; you review and publish manually through Substack's editor. The exception is Substack Notes: create_note and create_note_with_link publish short-form Notes immediately, because Notes have no draft state on Substack. Treat the Note tools as public-publish actions — there is no preview step and no undo from this server. The split is proportionate review, the piece of trust infrastructure for agents this server cares most about: the high-stakes surface gets a human gate, and the exception is stated loudly.

This server imposes no Bestseller-status check. Use an authenticated account with permission to manage the publication; individual operations depend on your Substack access. Connect through local stdio or self-hosted HTTP.

For 1.0 setup coverage and account eligibility, see compatibility. Maintainers can use the release checklist and distribution inventory.

Contents: Quick start · Setup · Tools · Operator CLI · Draft workflow · Analytics to draft · Export · Markdown · Multiple publications · Transports · Compatibility

Quick start

  1. Install and sign in. Browser login needs the optional Playwright dependency:

    npm install @conorbronsdon/substack-mcp playwright
    npx playwright install chromium
    npx substack-mcp login https://yourblog.substack.com --user-id 12345
    

    Use your own account's user ID (how to find it). The session is saved only after a bounded authenticated read succeeds. To paste credentials instead, see Option B.

  2. Verify a read. npx substack-mcp doctor --json --check-auth makes one bounded read per publication. It confirms read access, not your user ID or write permission.

  3. Connect your MCP client. Add the server to Claude Desktop or Claude Code, or use the Codex plugin. Environment variables take precedence; omit them to use the stored browser-login session. Then ask your assistant: "How many Substack subscribers do I have?"

  4. Prepare a draft for review. Follow the draft workflow: create_draft, search_posts, export_draft, preflight_draft, then plan_draft_update and update_draft. Long-form posts stay unpublished drafts until you publish them in Substack's editor. create_note and create_note_with_link publish immediately.

Community walkthrough

Jonathan Price's I used Codex to connect ChatGPT to Substack. Then it drafted this post. walks through using Codex to install the MCP locally, connecting ChatGPT through OpenAI's Secure MCP Tunnel, and creating a private draft for manual publication. He used the connection to create the draft of the guide itself.

The guide documents his September 18, 2026 setup with version 1.2.0. Its reported get_post 404 is fixed in 1.2.1. Client interfaces and access requirements can change; use the setup instructions below for this package's current configuration. This is a community walkthrough, not a hosted service provided by this project.

Setup

Requires Node.js 22 or newer (CI covers Node 22 and 24). Browser login additionally requires Playwright.

You can supply credentials two ways: paste them as env vars (below), or run the optional browser login which captures and stores them for you.

Option A — Browser login (optional, no manual cookie copying)

Install the server and optional Playwright dependency together in a local tools directory, then sign in:

npm install @conorbronsdon/substack-mcp playwright
npx playwright install chromium
npx substack-mcp login https://yourblog.substack.com --user-id 12345

substack-mcp-login remains a supported alias. Missing publication URL and user ID are prompted. Supply your own account's user ID (steps below); a post author's byline does not verify your identity. The browser opens for sign-in, including any CAPTCHA. Login then opens <publication>/publish/home and waits for the publication session cookie; on custom domains this is connect.sid on the custom domain. A bounded authenticated read must succeed before saving. This verifies read access, not the configured user ID or permission to write.

Without --profile, login saves ~/.substack-mcp/session.json (directory override: SUBSTACK_MCP_HOME). The server uses this legacy session when publication credential environment variables and SUBSTACK_PROFILES are unset.

Default storage (SUBSTACK_CREDENTIAL_STORE=file): sessions use AES-256-GCM with a key derived from the OS account and machine. File permissions request 0600; Windows access also depends on directory ACLs. This is a machine-bound file, not an OS keychain or secret vault. Code running as your OS user can derive the key. Use environment credentials if your MCP client manages secrets for you.

Optional OS keychain: set SUBSTACK_CREDENTIAL_STORE=keychain in both the login process and the MCP client's environment. macOS uses Keychain via /usr/bin/security; Linux needs libsecret and secret-tool plus an unlocked Secret Service; Windows uses Credential Manager through PowerShell's PasswordVault. All three are exercised with synthetic credentials by the keychain CI workflow on GitHub-hosted runners (an unlocked temporary macOS keychain and a gnome-keyring session on Linux); desktop setups with locked keychains may still prompt. Login writes the selected account to the keychain, and the server reads it there. Explicit keychain selection never reads the encrypted file as a fallback. The keychain helps against other OS users, copied disks, and some malware limited to file access. Code running as your user can usually query the keychain. Keep the OS account and running code trusted. On Linux, secret-tool lookup can exit 1 without an error message for either an absent entry or a locked keyring. Named writes without --force search and unlock first, and refuse overwrites when an entry is found.

Named profiles and migration
npx substack-mcp login https://yourblog.substack.com --user-id 12345 --profile work
npx substack-mcp profiles list
# Copy an existing legacy session without changing its file:
npx substack-mcp profiles migrate --name personal
# Copy a file session or named file profile into the keychain; source remains:
npx substack-mcp profiles migrate --to keychain
npx substack-mcp profiles migrate --to keychain --name work

Keys start with a lowercase ASCII letter and contain only lowercase letters, digits and hyphens, up to 64 characters. Existing profiles require explicit --force to replace. List output contains keys, readability status, publication origins and file save times; it excludes cookies and user IDs. Unreadable profiles remain visible but cannot be selected. Save time records local persistence, including migration; it is not token issuance or expiration time. Listing is bounded to 32 profiles.

Profile storage requires a local filesystem supporting hard links (such as NTFS or a typical Linux filesystem), so creation can install a complete encrypted file without overwriting an existing name. FAT/exFAT and some network mounts are not supported: set SUBSTACK_MCP_HOME to a suitable local directory. Do not use --force to work around an unsupported filesystem.

Set SUBSTACK_PROFILES=work,personal in your MCP client's environment to select up to 32 distinct profiles. Remove all publication credential variables first: combining profile selection with legacy or named credential variables is an error, including empty variables. Missing, corrupt or invalid selected profiles stop startup; they never fall back to another account. Profiles on disk are never activated by discovery. With multiple profiles, tools require an explicit publication key and CLI reads require --publication.

To roll back, unset SUBSTACK_PROFILES and restore your previous environment configuration. Migration preserves the legacy session byte-for-byte. These files use the existing encryption format. profiles list lists file profiles; select keychain profiles explicitly with SUBSTACK_PROFILES after migration or login. Run substack-mcp status --json for offline configuration diagnostics or substack-mcp doctor --check-auth --json for a bounded read per selected account.

Option B — Get your credentials manually

Open your Substack in a browser, then:

  1. Session token: For custom domains, open DevTools → Application → Cookies → https://your-custom-domain and copy connect.sid from the publication's own domain (URL-encoded string starting with s%3A). Do not copy substack.sid from substack.com: it is a different session that the custom domain's admin API rejects with 403. For *.substack.com publications, use connect.sid on that host or substack.sid on .substack.com. The token must match the host of SUBSTACK_PUBLICATION_URL, with the .substack.com fallback only for Substack-hosted publications.
  2. User ID: Follow Find your account user ID. Do not use a publication post's byline ID: publications can have multiple authors. This server does not independently verify the supplied ID.
  3. Publication URL: Your Substack URL, including custom domain if you have one (e.g., https://newsletter.yourdomain.com or https://yourblog.substack.com)

Find your account user ID

  1. In Substack, open your own account profile and copy its handle (the part after @ in https://substack.com/@your-handle). A publication URL or another author's profile is not your account handle.
  2. Open https://substack.com/api/v1/user/your-handle/public_profile with that handle substituted. Read the top-level numeric id in the JSON response; do not use an ID nested under a publication or post byline. This public profile endpoint is also used by this server's anonymous profile lookup.
  3. Confirm that the response's handle and name match the account you will sign in with. Supply the top-level id to login --user-id or SUBSTACK_USER_ID. A public profile lookup identifies the handle you entered; the browser-login read check does not prove that the cookie belongs to that ID. Recheck the account before any write operation.

2. Configure your MCP client

Claude Desktop

Add to your claude_desktop_config.json:

{
  "mcpServers": {
    "substack": {
      "command": "npx",
      "args": ["-y", "@conorbronsdon/substack-mcp"],
      "env": {
        "SUBSTACK_PUBLICATION_URL": "https://yourblog.substack.com",
        "SUBSTACK_SESSION_TOKEN": "your-session-token",
        "SUBSTACK_USER_ID": "your-user-id"
      }
    }
  }
}

Claude Code and Codex plugin setup is available alongside manual MCP configuration. The repository marketplace and local plugin are separate from curated-directory acceptance or hosted ChatGPT support.

Claude Code

Add to your .mcp.json:

{
  "mcpServers": {
    "substack": {
      "command": "npx",
      "args": ["-y", "@conorbronsdon/substack-mcp"],
      "env": {
        "SUBSTACK_PUBLICATION_URL": "https://yourblog.substack.com",
        "SUBSTACK_SESSION_TOKEN": "your-session-token",
        "SUBSTACK_USER_ID": "your-user-id"
      }
    }
  }
}

3. Verify

Ask your AI assistant: "How many Substack subscribers do I have?"

Tool output compatibility, response limits and versioning are documented in the tool contract.

Operator diagnostics

The CLI exposes operator commands and configuration checks through the same MCP handlers and credential resolution as the server. drafts create writes one private draft; the other operator commands read:

substack-mcp status --json
substack-mcp doctor --json --check-auth
substack-mcp drafts list --limit 10

Commands, the JSON envelope, exit codes, request deadlines and response limits are documented in operator CLI and diagnostics.

Tools

Every tool declares explicit MCP side-effect annotations; see tool annotations. Tool descriptions carry the authoritative wording.

Read

ToolDescription
get_subscriber_countGet your publication's current subscriber count
list_subscribersRead a bounded page of private subscriber records
search_subscribersFilter and page private subscriber records; optional activity, date, flags and revenue fields
get_subscriberLook up membership by exact email; reconcile pending additions
list_published_postsList published posts with pagination
get_publicationRead projected publication identity/settings, verify the configured host, and report missing fields; does not verify account identity or role
list_publication_tagsRead tag definitions, including hidden tags by default, with bounded local pagination
get_post_tagsResolve post tag associations; preserves unresolved IDs and reports empty-result identity uncertainty. Draft coverage is currently live-verified only for empty responses
search_postsSearch a publication archive by query and status; bounded pages with continuation metadata
plan_draft_updateReview proposed changes, preflight and a receipt for best-effort stale detection; no writes
export_draftEditable Markdown, exact original body, conversion diagnostics, preflight and editor link
preflight_draftRead-only checks for title, audience, body structure, images, and paywalls, with an editor link
list_draftsList draft posts
get_postGet full content of a published post by ID
get_draftGet full content of a draft by ID
get_post_commentsGet comments on a published post
get_sectionsList your publication's sections (categories) with their IDs
get_post_analyticsGet a published post's stats (views, opens, signups, subscribes, reactions) by ID
rank_postsRank posts by views, opens, sends, rates, signups, subscribes, estimated value or date, keeping null and missing values distinct
get_publication_statsRead dashboard summary and ranged publication metrics, with missing and unavailable states
get_growth_sourcesRead bounded growth source attribution and optional events
list_incoming_recommendationsList publications that recommend yours, with start date, active state and dashboard-attributed subscribers
list_outgoing_recommendationsList whom your publication recommends (can be partial). This is not who recommends it; use list_incoming_recommendations for that
list_outgoing_recommendation_statsList subscribers your publication has sent to the publications it recommends
list_scheduled_postsList posts scheduled for future publication (read-only; scheduling stays in Substack's editor)
get_user_profileRead a minimal public user profile by handle, anonymously
get_profile_feedRead one public profile feed page with cursor continuation, anonymously
get_note_threadRead a public Note, ancestors and one replies page, anonymously
list_public_postsRead a bounded public archive page, anonymously
get_public_postRead an anonymous public post by URL with body status and truncation flags

Public reading

These five tools use a separate anonymous reader. It sends only User-Agent and Accept, never the configured publication cookie or other credentials. The configured publication selects the default archive origin; callers may supply an allowlisted HTTPS publication origin. Allowed hosts are substack.com, one-label *.substack.com, configured publication origins, and exact origins in SUBSTACK_PUBLIC_READ_ORIGINS (comma-separated HTTPS origins without ports, paths or userinfo). Redirects are rejected. A *.substack.com publication may redirect to its custom domain (for example, lenny.substack.com); JSON reads do not follow that redirect. Add the custom HTTPS origin to SUBSTACK_PUBLIC_READ_ORIGINS and read through that origin. With multiple publications, the publication key remains required for every tool.

Profile feed pages are upstream-sized; has_more: null means the upstream omitted continuation metadata. Thread pages can omit replies when more_branches or next_cursor is present; completeness: "unknown" means continuation metadata was omitted. Archive full pages have has_more: null because no total is returned. Public post body_status is a heuristic based on audience and body presence; it does not establish full access. The anonymous reader does not use subscription entitlements. Reader subscriptions and inbox are unsupported: the configured publication session received 401 on the substack.com reader-account routes, which require a separate reader session this server does not manage.

Archive search and draft review

search_posts accepts query (1–500 characters), status (published, drafts, or scheduled, default published), offset (default 0), and limit (1–50, default 25). It makes one authenticated archive request and returns projected metadata, returned, total, has_more, and next_offset. Continue with next_offset and the same query/status. When Substack omits the total and a page is full, has_more is null (unknown); another page may be empty. Substack controls matching and indexing: this is not a guaranteed full-text scan. Use get_post or get_draft to retrieve full content. Pagination is not a snapshot; concurrent edits can move results between pages.

preflight_draft accepts draft_id, reads it once, and returns checks_passed, findings with severity/code/message, content counts and an editor link. It checks title, audience, JSON/body shape, image wrappers and HTTPS sources, and paywall count and edge placement. Unknown nodes and external images produce review warnings. Bodies over two million characters, 10,000 nodes, or depth 100 are not fully checked; counts.complete is false and aggregate checks are skipped after a scan limit. Unknown-node warnings name up to five types for editor review. This is a focused static check, not full ProseMirror validation or publish approval. It does not fetch links/images, verify access settings, or prove final rendering. Review the draft in Substack; no content is modified.

Both tools require publication when multiple publications are configured.

Write (private drafts; image upload returns a public URL)

ToolDescription
create_draftCreate a new draft from markdown (private)
update_draftApply a reviewed change receipt; recheck unpublished state and report readback outcomes
update_draft_tagsPlan or change draft tags; dry-run by default, draft-only, with one readback after writes
upload_imageUpload an image to Substack's CDN from a file, data URI or public HTTPS URL — returns a publicly-fetchable (unlisted) URL

Review before changing a draft

In 0.9, call plan_draft_update, review its output, then call update_draft with the same fields and returned receipt. Published or known stale drafts are rejected. The read/write race remains; check readback outcomes and review in Substack. The CLI shares this flow through drafts plan and drafts apply. See draft changes and migration for examples and limits.

Publish (Notes — public immediately)

ToolDescription
create_notePublish a Substack Note (short-form, publishes immediately)
create_note_with_linkPublish a Note with a link card attachment (publishes immediately)

Notes have no draft state on Substack, so there is no draft-first option for these two tools.

Subscriber management

add_free_subscriber adds one consenting reader to the free newsletter. It is a distribution change: that reader may receive future newsletter emails. It can request a welcome email with send_welcome_email: true (off by default). It never grants paid access or overrides Substack's suppression of previously unsubscribed addresses. Its MCP annotations identify it as an external write (readOnlyHint: false, openWorldHint: true).

{"email":"reader@example.org","consent_confirmed":true,"consent_evidence":{"source":"booking:message-id","recorded_at":"2026-09-01T00:00:00Z"},"dry_run":true}

Dry-run is the default. After checking actual newsletter consent, set dry_run: false to execute. Live adds require the source reference and timestamp in consent_evidence; this attestation is echoed with the publication key for auditing and does not replace checking the underlying consent record. Multi-publication configurations also require the publication selector, just like every other tool.

Shortened here. Read the whole README on GitHub.

Signals

GitHub stars
38
Forks
9
Last commit
Sep 2026
Weekly_downloads
311 weekly_downloads
Advanced
Delivery
substack-mcp MCP server → your Ahel connector (mcp.ahel.ai) → your AI.
Item type
mcp-server
Key
io-github-conorbronsdon-substack-mcp
Source
github.com/conorbronsdon/substack-mcp