Skip to content

Latest commit

 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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.

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.

About

Substack MCP server for Claude Code — articles, Notes, cover images via the substack.sid cookie.

Resources

Stars

15 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages