Skip to main content
Glama
README.md
# 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`](https://github.com/ma2za/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

```bash
# 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:

```bash
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:

```text
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):

```bash
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:

```text
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](https://developers.openai.com/api/docs/guides/secure-mcp-tunnels).
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:

```bash
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](https://skills.sh/)-compatible **agent skill**
that uses the MCP above to plan, draft, generate a thumbnail, and publish a
Substack article in one flow.

```bash
# 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](https://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](https://pypi.org/project/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

```bash
# 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.

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

ActivityMaintained
ResponsivenessResponsive