Skip to main content
Glama

substack-mcp

A Model Context Protocol (MCP) server for Substack. Lets Claude Code create drafts, upload images, set cover thumbnails, schedule, and publish posts on your Substack publication.

Built on top of python-substack. Uses Substack's internal API (no public posting API exists). Not affiliated with Substack Inc.

Tools

Required

  • create_draft(title, content_markdown, subtitle?, audience?) — Create a new draft from Markdown.

  • update_draft(post_id, title?, subtitle?, content_markdown?, audience?) — Edit an existing draft.

  • upload_image(image_path) — Upload a local file or remote URL to Substack's CDN, returning the URL.

  • publish_draft(post_id, send_email?, share_automatically?) — Publish immediately. send_email toggles email delivery.

Recommended

  • schedule_draft(post_id, iso_datetime) — Schedule a publish for a future date/time (ISO 8601).

  • unschedule_draft(post_id) — Cancel a scheduled publish.

  • set_cover_image(post_id, image_url) — Set the cover thumbnail (from upload_image URL).

Utility

  • list_drafts(limit?) — List recent drafts.

  • get_draft(post_id) — Get a draft's full body.

  • delete_draft(post_id) — Permanent deletion.

Related MCP server: Substack MCP Server

Setup

# 1. Install dependencies
uv pip install -e .

# 2. Make sure you're logged in to Substack in Chrome (or Brave/Edge) — that's it.

# 3. Save credentials — auto-detects your existing browser session
substack-mcp-setup

# 4. Register with Claude Code
claude mcp add substack-mcp --scope user -- /Users/$USER/substack/.venv/bin/substack-mcp

Restart Claude Code, then /mcp should show substack-mcp as connected.

Run fully in the cloud with Cloudflare

The Cloudflare deployment exposes a stable, OAuth 2.1-protected Streamable HTTP endpoint at https://<worker>.workers.dev/mcp. ChatGPT performs the writing and reasoning; the Worker and its on-demand Python Container only execute Substack operations. No OpenAI API key, Secure MCP Tunnel, or always-on computer is required.

Requirements:

  • a Cloudflare account with Workers Containers enabled

  • Node.js 22.18 or newer

  • a GitHub OAuth App used only to verify who may connect

  • a current substack.sid session token (store it as a Cloudflare secret)

Install and create the OAuth state store:

npm install
npx wrangler login
npx wrangler kv namespace create OAUTH_KV

Put the returned namespace ID in wrangler.jsonc in place of REPLACE_WITH_OAUTH_KV_ID. Your final Worker URL will normally be:

https://substack-mcp.<your-workers-subdomain>.workers.dev

Create a GitHub OAuth App with that URL as its homepage and https://substack-mcp.<your-workers-subdomain>.workers.dev/callback as its authorization callback URL. Then store all private values using Wrangler's interactive secret prompt (never commit them or paste them into chat):

npx wrangler secret put GITHUB_CLIENT_ID
npx wrangler secret put GITHUB_CLIENT_SECRET
npx wrangler secret put ALLOWED_GITHUB_LOGINS
npx wrangler secret put SUBSTACK_PUBLICATION_URL
npx wrangler secret put SUBSTACK_SESSION_TOKEN
npm run deploy

ALLOWED_GITHUB_LOGINS is a comma-separated allowlist, for example nanameru. SUBSTACK_PUBLICATION_URL is the full publication URL. For the session token, copy only the value of the substack.sid cookie from a browser that is logged in to Substack. The browser is needed once for setup; it does not need to remain open afterward. Signing out of all Substack sessions invalidates this credential, so update the Cloudflare secret after doing that.

In ChatGPT developer mode, create an app/connector from:

https://substack-mcp.<your-workers-subdomain>.workers.dev/mcp

Choose OAuth when prompted and sign in with an allowlisted GitHub account. Draft creation is non-public. Publishing, subscriber email, draft deletion, and Notes remain confirmation-gated by the tool descriptions and the bundled substack-article skill.

Connect to ChatGPT Work with Secure MCP Tunnel

This server uses the local substack.sid browser session and should not be published directly on the internet. For a private ChatGPT Work connection, use OpenAI Secure MCP Tunnel. The tunnel keeps the stdio MCP and Substack credential on your Mac while making the tools available to an authorized ChatGPT workspace.

Prerequisites:

  • macOS with Chrome, Brave, Edge, Chromium, Vivaldi, or Opera already signed in to Substack

  • ChatGPT developer mode enabled

  • an OpenAI Platform tunnel ID associated with the target ChatGPT workspace

  • tunnel-client installed from OpenAI Platform tunnel settings

  • a tunnel runtime API key with Tunnels Read + Use permission

Run:

git clone https://github.com/nanameru/substack-mcp.git
cd substack-mcp

# Keep these values out of shell history when possible. Never commit them.
export SUBSTACK_TUNNEL_ID="tunnel_..."
export CONTROL_PLANE_API_KEY="sk-..."

./scripts/setup-chatgpt-work-tunnel.sh
tunnel-client run --profile substack-mcp

Then open ChatGPT Plugins, create a developer-mode app, choose Tunnel under Connection, and select the tunnel. The Mac and tunnel-client process must stay running while ChatGPT calls the Substack tools.

The helper never prints or uploads the Substack session token. It runs substack-mcp-setup locally, stores the credential with 0600 permissions, and points the tunnel at the local stdio command.

(Optional) Install the substack-article skill

This repo also ships a Vercel Skills-compatible agent skill that uses the MCP above to plan, draft, generate a thumbnail, and publish a Substack article in one flow.

# Globally for all projects
npx skills add nanameru/substack-mcp -g -a claude-code -y

# Or scoped to the current project
npx skills add nanameru/substack-mcp -a claude-code

Restart Claude Code. The skill is automatically invoked when you say things like 「Substack 記事を書いて」 / 「Substack に投稿して」 / 「短文ポストして」.

npx skills works with Claude Code, Cursor, Codex, OpenCode, GitHub Copilot, and 50+ other agents. See skills.sh for details.

The skill expects:

  • substack-mcp registered as an MCP (above)

  • codex MCP registered (for thumbnail generation; optional if you skip thumbnails)

  • macOS with Hiragino fonts installed (default on macOS for Japanese text overlay)

See skills/substack-article/SKILL.md for the full flow and guardrails.

How auth works

By default substack-mcp-setup reads the substack.sid cookie directly from your existing Chrome session via pycookiecheat. Substack can't tell anything was automated because nothing was: it's the same session you're already using.

macOS will prompt once for Keychain access ("Chrome Safe Storage"). Click "Always Allow" so it doesn't ask again next time.

Supports: Chrome, Brave, Edge, Chromium, Vivaldi, Opera.

Fallback modes

# Specific browser
substack-mcp-setup --from-browser brave

# Playwright-based (often blocked by Substack — use --chrome instead)
substack-mcp-setup --browser

# Manual paste from DevTools
substack-mcp-setup --manual

Tokens are stored at ~/Library/Application Support/substack-mcp/config.json with 0600 permissions.

Security

The substack.sid cookie is equivalent to a password — anyone with it has full account access (publish posts, edit billing, etc.). Treat it as such.

Where the token lives

  • macOS: ~/Library/Application Support/substack-mcp/config.json (mode 0600)

  • Linux: ~/.config/substack-mcp/config.json (mode 0600)

  • Or via env vars: SUBSTACK_PUBLICATION_URL + SUBSTACK_SESSION_TOKEN (env vars are inherited by child processes — be aware when spawning subprocesses)

The .gitignore excludes config.json; never commit it. The MCP also writes a temporary cookie file via tempfile.mkstemp (mode 0600) and deletes it in a finally block — see auth.py:write_cookie_file.

If a token leaks

  1. Sign out of all sessions: Substack → Settings → Security → "Sign out of all sessions". This invalidates every existing substack.sid immediately.

  2. Log back in to Substack in your browser.

  3. Re-run substack-mcp-setup to capture the new cookie.

Image upload safety

upload_image only accepts:

  • HTTP(S) URLs, or

  • Local files with image extensions (.png, .jpg, .jpeg, .gif, .webp, .heic, .heif) that are not under sensitive system paths (/etc, /System, ~/.ssh, ~/.aws, ~/Library/Keychains, etc.)

This guards against an assistant being tricked (via prompt injection in fetched content) into uploading e.g. an SSH private key to Substack's CDN.

Known limitation: Markdown image syntax ![alt](path) inside create_draft is processed by python-substack and bypasses this validation. If you pass untrusted Markdown, sanitize image paths first.

Dependencies

Versions are pinned with ~= (compatible release, no major bumps). Bumping python-substack in particular should be reviewed — it talks to Substack's private API and lives outside Substack's official surface.

Notes

  • audience accepts: everyone (default), only_paid, founding, only_free.

  • Markdown image syntax ![alt](path/or/url) auto-uploads local files when you call create_draft.

  • The cover image (set via set_cover_image) is what appears on your publication homepage and in social shares. If you don't set one explicitly, Substack typically uses the first image in the body.

Available Tools

11 tools
create_draftA

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 (alt - 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.

Inputs: title (required), content_markdown (required), subtitle, audience.

delete_draftA

Permanently delete a draft. This cannot be undone.

Args: post_id: Draft ID.

Returns: {post_id, deleted: true}

Inputs: post_id (required).

get_draftA

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).

Inputs: post_id (required).

list_draftsA

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.

Inputs: limit. Returns: result

post_noteA

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}

Inputs: text (required).

publish_draftA

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}

Inputs: post_id (required), send_email, share_automatically.

schedule_draftA

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}

Inputs: post_id (required), iso_datetime (required).

set_cover_imageA

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.

Inputs: post_id (required), image_url (required).

unschedule_draftB

Cancel a scheduled publish, keeping the post as a draft.

Args: post_id: Draft ID.

Returns: {post_id}

Inputs: post_id (required).

update_draftA

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.

Inputs: post_id (required), title, subtitle, content_markdown, +1 more.

upload_imageA

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 alt.

Inputs: image_path (required).

Tool Schema Changelog

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

Loading tool schema changelog…

    TDQS

    A4/5.0

    Scored across 11 tools

    Disambiguation5/5

    Each tool has a clearly distinct purpose: draft CRUD, publishing, scheduling, notes, and image operations. No two tools overlap in functionality.

    Naming Consistency5/5

    All tools follow a consistent verb_noun pattern with underscores (e.g., create_draft, publish_draft, upload_image), making the API predictable.

    Tool Count5/5

    11 tools is well-scoped for a Substack server, covering drafting, publishing, scheduling, notes, and images without being excessive.

    Completeness4/5

    The tool set covers core drafting and publishing workflows comprehensively, but lacks tools for managing published posts, subscribers, or analytics, which are minor gaps for a content creation MCP.

    Maintenance

    ActivityNo data
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      Not graded
      maintenance
      An MCP server that enables AI assistants like Claude to interact with Substack newsletters, allowing for post retrieval, content searching, and author information access through a standardized interface.
      MIT
    • A
      license
      A
      quality
      A
      maintenance
      MCP server and CLI for Substack creators: rich Markdown drafts, draft tags, Notes, publication and post analytics, growth sources, archive search, anonymous reading of public profiles and Note threads, subscriber segmentation and consented subscriber management. Long-form posts stay drafts; Notes publish immediately.
      34
      1,542 npm
      46
      MIT