
Security News
White House Authorizes Private Companies to Conduct Offensive Cyber Operations
A new federal program will let vetted U.S. cybersecurity firms help investigate and disrupt foreign cybercrime groups under government direction.
@conorbronsdon/substack-mcp
Advanced tools
MCP server for Substack — read posts, manage drafts, publish Notes. Long-form posts are draft-only by design (no publish, no delete); Notes publish immediately.
An MCP server for Substack. Read your publication data and manage drafts from your AI agent. Long-form posts are draft-only by design — no publish, no delete. Short-form Notes publish immediately.

An MCP server for Substack that lets AI assistants read your publication data and manage drafts. The draft list shown in the demo above is sample data, not real account values.
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.
Built and maintained by Conor Bronsdon for the Chain of Thought podcast production workflow, where it drafts and reviews newsletter posts before a human hits publish. Conor hosts Chain of Thought, a show about AI infrastructure and how practitioners actually build with it. More tools for creators live in ai-tools-for-creators. Find Conor on X at @ConorBronsdon.
Sibling MCP servers:
Every tool declares MCP tool annotations, set explicitly rather than left to MCP's defaults (an omitted destructiveHint or openWorldHint defaults to true). Reads carry readOnlyHint: true. Every write is additive, so all writes carry destructiveHint: false. Draft writes are private (openWorldHint: false); upload_image carries openWorldHint: true because it returns a publicly-fetchable CDN URL; and the Note tools carry openWorldHint: true for immediate public publish. Annotations are untrusted hints, so the authoritative wording lives in each tool's description.
| Tool | Description |
|---|---|
get_subscriber_count | Get your publication's current subscriber count |
list_published_posts | List published posts with pagination |
list_drafts | List draft posts |
get_post | Get full content of a published post by ID |
get_draft | Get full content of a draft by ID |
get_post_comments | Get comments on a published post |
get_sections | List your publication's sections (categories) with their IDs |
get_post_analytics | Get a published post's stats (views, opens, signups, subscribes, reactions) by ID |
list_scheduled_posts | List posts scheduled for future publication (read-only; scheduling stays in Substack's editor) |
| Tool | Description |
|---|---|
create_draft | Create a new draft from markdown (private) |
update_draft | Update an existing draft (unpublished only; private) |
upload_image | Upload an image to Substack's CDN — returns a publicly-fetchable (unlisted) URL |
| Tool | Description |
|---|---|
create_note | Publish a Substack Note (short-form, publishes immediately) |
create_note_with_link | Publish 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.
list_scheduled_posts reads what you've queued there, but this server never creates, edits, or cancels a schedule.)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.
Removes the DevTools cookie hunt and the ~90-day re-copy. Playwright is not bundled (it's large), so install it once, then sign in:
npm i -g playwright && npx playwright install chromium
npx --package @conorbronsdon/substack-mcp substack-mcp-login https://yourblog.substack.com
A browser opens; sign in to Substack (CAPTCHA included). The tool captures your
session cookie, auto-resolves your user id, and writes them to
~/.substack-mcp/session.json (override the directory with SUBSTACK_MCP_HOME).
The MCP server reads that file automatically whenever the SUBSTACK_* env vars
are unset — so with browser login you can omit the env block entirely.
Storage & security: the file is written 0600 and encrypted with AES-256-GCM
under a key derived from this OS account + machine (never stored). A copied file
is useless elsewhere and casual disk/backup reads see only ciphertext. This is
machine-binding + obfuscation, not a secret vault — code running as you on
this machine can re-derive the key (the same caveat as the plaintext env-var
path). If you prefer, use Option B and let your MCP client handle the secret.
Open your Substack in a browser, then:
connect.sid (URL-encoded string starting with s%3A)fetch('/api/v1/archive?sort=new&limit=1').then(r=>r.json()).then(d=>console.log(d[0]?.publishedBylines?.[0]?.id))https://newsletter.yourdomain.com or https://yourblog.substack.com)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"
}
}
}
}
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"
}
}
}
}
Ask your AI assistant: "How many Substack subscribers do I have?"
Substack session tokens expire periodically (typically ~90 days). If you get authentication errors, grab a fresh connect.sid cookie from your browser and update the env var (make sure ad blockers are disabled when copying the cookie) — or, if you used the browser login, just re-run substack-mcp-login to refresh the stored session.
Substack publications served on a custom domain (e.g. blog.example.com) sit behind Cloudflare, which can reject non-browser requests with 403 error code: 1010. To avoid this, the server sends a browser User-Agent and a Referer by default, and addresses the publication by its canonical *.substack.com host.
SUBSTACK_PUBLICATION_URL to the publication's *.substack.com address rather than the custom domain. Calls to the canonical host are served directly; custom-domain calls may 301-redirect and then 401.SUBSTACK_USER_AGENT if you need a different browser signature:"env": {
"SUBSTACK_PUBLICATION_URL": "https://yourblog.substack.com",
"SUBSTACK_SESSION_TOKEN": "your-session-token",
"SUBSTACK_USER_ID": "your-user-id",
"SUBSTACK_USER_AGENT": "Mozilla/5.0 ..."
}
Every request to Substack is bounded by a 30-second deadline. Node applies no request timeout of its own — only a 10-second connect timeout — so a host that accepts the connection and then goes silent (a proxy that drops packets rather than refusing them) would otherwise hang a tool call indefinitely. A request that hits the deadline fails with a TimeoutError naming the endpoint and the limit.
Raise or lower it with SUBSTACK_REQUEST_TIMEOUT_MS (milliseconds; a non-numeric or non-positive value is ignored with a warning and the default is used):
"env": {
"SUBSTACK_REQUEST_TIMEOUT_MS": "60000"
}
API failures are mapped to a typed error hierarchy (SubstackAPIError base, with AuthenticationError, RateLimitError, ValidationError, NotFoundError, and ServerError subclasses keyed off HTTP status) in src/utils/errors.ts. Every tool call still surfaces the same error response shape on failure — the typed hierarchy just makes the message specific to what went wrong instead of a single generic "Substack API error" string.
| Class | Status | Triggered by |
|---|---|---|
AuthenticationError | 401/403 | Expired/invalid session token, or a Cloudflare error code: 1010 block (see above) |
RateLimitError | 429 | Too many requests against the Substack API in a short window |
ValidationError | 400 | Malformed or invalid arguments passed to a tool (e.g. a missing required field) |
NotFoundError | 404 | The referenced draft, post, or note doesn't exist |
ServerError | 5xx | Failure on Substack's side |
TimeoutError | 408 (synthetic) | The request hit the client's own deadline — no response arrived, so there is no real status to report (see Request timeout) |
SubstackAPIError | any other status | Fallback for unmapped status codes |
Substack error response bodies are inconsistent — sometimes JSON ({"error": "..."} or {"errors": [...]}), sometimes plain text, and sometimes a large Cloudflare HTML block page. extractErrorDetail handles all three: it tries JSON.parse first, falls back to the raw text (trimmed and capped at ~500 characters so a multi-KB HTML page doesn't become the whole error message), and only uses a generic fallback string if the body is empty.
The create_draft and update_draft tools accept markdown and convert it to Substack's native format. Supported:
inline codeTables: Substack's post editor has no table node, so a markdown table cannot be rendered natively. Rather than mangle the pipes into a paragraph, a detected GFM table is preserved verbatim inside a code block — the content survives so you can reformat it (as an image or embed) in Substack's editor.
SUBSTACK_SESSION_TOKEN secure.SIGTERM and SIGINT are handled: the server closes its transport and exits 0, so docker stop returns promptly instead of waiting out the grace period.git clone https://github.com/conorbronsdon/substack-mcp.git
cd substack-mcp
npm install
npm run build
Run locally:
SUBSTACK_PUBLICATION_URL=https://yourblog.substack.com \
SUBSTACK_SESSION_TOKEN=your-token \
SUBSTACK_USER_ID=your-id \
npm start
Issues and pull requests are welcome. Because this server uses Substack's unofficial API, the most useful contributions are fixes when an endpoint changes. If a tool stops working, open an issue with the tool name and the error. The safe-by-design boundary stays: no publish, no delete, no schedule for long-form posts. Notes publish immediately by design and must keep saying so loudly in their descriptions.
This is an independent personal project, not affiliated with, sponsored by, or endorsed by any company. All views expressed are my own.
MIT
FAQs
MCP server for Substack — read posts, manage drafts, publish Notes. Long-form posts are draft-only by design (no publish, no delete); Notes publish immediately.
The npm package @conorbronsdon/substack-mcp receives a total of 108 weekly downloads. As such, @conorbronsdon/substack-mcp popularity was classified as not popular.
We found that @conorbronsdon/substack-mcp demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
A new federal program will let vetted U.S. cybersecurity firms help investigate and disrupt foreign cybercrime groups under government direction.

Research
/Security News
The campaign amassed more than 75,000 installs by targeting Russian-speaking users seeking access to blocked services.

Company News
Open source maintainers are under more pressure than ever. We're raising our open source program from the Team plan to the Business plan, free.