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.
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_emailtoggles 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 (fromupload_imageURL).
Utility
list_drafts(limit?)— List recent drafts.get_draft(post_id)— Get a draft's full body.delete_draft(post_id)— Permanent deletion.
# 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-mcpRestart Claude Code, then /mcp should show substack-mcp as connected.
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.sidsession 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_KVPut 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 deployALLOWED_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.
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-clientinstalled 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-mcpThen 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.
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-codeRestart 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-mcpregistered as an MCP (above)codexMCP 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.
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.
# 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 --manualTokens are stored at ~/Library/Application Support/substack-mcp/config.json
with 0600 permissions.
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.
- macOS:
~/Library/Application Support/substack-mcp/config.json(mode0600) - Linux:
~/.config/substack-mcp/config.json(mode0600) - 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.
- Sign out of all sessions: Substack → Settings → Security → "Sign out of
all sessions". This invalidates every existing
substack.sidimmediately. - Log back in to Substack in your browser.
- Re-run
substack-mcp-setupto capture the new cookie.
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  inside create_draft
is processed by python-substack and bypasses this validation. If you pass
untrusted Markdown, sanitize image paths first.
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.
audienceaccepts:everyone(default),only_paid,founding,only_free.- Markdown image syntax
auto-uploads local files when you callcreate_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.