Ask Claude what's new in your Substack subscriptions, and have it read the posts for you.
Quick start · Tools · Daily digest · Headline ranking with Jev · Logging in · Troubleshooting
An MCP server for your own Substack account:
- Read: your subscriptions, a combined feed, full posts (including paid ones you have access to), and search.
- Your reading activity: reading history with how far you read each post, plus saved and hearted posts.
- Chats: publication chats and direct messages.
- Act, when you ask: subscribe to and unsubscribe from free newsletters.
- Daily digest: with Hermes Agent, a scheduled morning digest. Jev can rank posts against your interests before they're read.
- No API keys for Substack: you log in once in a browser window.
| What | Needed? |
|---|---|
| Your Substack account | Yes. Free and paid subscriptions both work |
| Node.js 20+ | Yes |
| Chrome or Edge | Only for the browser login (you can paste a cookie instead) |
| A Jev-style decision model | Optional. Ranks digest posts against your interests in a few seconds. Any provider with the same decisions API works, set by URL, key and model like an OpenAI-compatible client. The default is TypeSafe's Jev on OpenRouter, about $0.03 per 1,000 posts |
| An MCP client | Claude Code, Claude Desktop, Cursor, VS Code, Hermes Agent… |
Substack has no public API. The server calls the endpoints substack.com uses, with your session, so it sees only what you see logged in.
git clone https://github.com/wkbaran/substack-reader-mcp.git
cd substack-reader-mcp
npm install && npm run build
node dist/cli.js login # a browser window opens; sign in to Substack as usual
node dist/cli.js install # registers the server with Claude CodeRestart Claude Code, then try:
- "What's new in my Substack feed this week?"
- "Summarize the latest post from The Pragmatic Engineer."
- "Which of my saved posts did I never finish?"
- "What are people talking about in Nate's subscriber chat?"
- "Propose an interests.md from my Substack activity."
Other MCP clients (Claude Desktop, Cursor, VS Code)
Point the client at dist/cli.js with an absolute path, after running login:
{ "mcpServers": { "substack-reader": { "command": "node", "args": ["/absolute/path/to/substack-reader-mcp/dist/cli.js"] } } }- Claude Desktop:
claude_desktop_config.json(Settings → Developer → Edit Config) - Cursor:
~/.cursor/mcp.json - VS Code:
.vscode/mcp.json, using"servers"instead of"mcpServers"and adding"type": "stdio" - Claude Code, manually:
claude mcp add --scope user substack-reader -- node /absolute/path/to/dist/cli.js
How to name things:
- Publications: a name, part of a name ("pragmatic"), subdomain or URL.
- Posts: any Substack link, or a post id.
Reading (all read-only)
| Tool | What it returns |
|---|---|
get_feed |
Recent posts across all subscriptions, newest first. since takes "7d", "48h" or a date |
read_post |
A full post as Markdown. Paid posts you can't access are flagged as previews |
get_recent_posts · search_posts |
One publication's latest posts · keyword search within one publication |
list_subscriptions |
Every subscription, including hidden ones, with membership |
get_reading_history |
Posts you opened on the web or in the app, with how far you read each one, and whether you hearted or saved it |
get_saved_posts · get_liked_posts |
Posts you saved · posts you hearted |
interests_evidence |
What your activity says about your taste, for drafting an interests.md |
auth_status |
Who you're logged in as, and whether the session works |
Chat (read-only; Substack's unread flags aren't reliable, so these work by activity time)
| Tool | What it returns |
|---|---|
list_chats |
Publication chats you're in, and direct messages |
get_chat_activity |
Chats and DMs with activity since a time, optionally with the new messages |
get_chat_threads · read_chat_thread · read_dm |
A chat's threads · one thread with its replies · one DM conversation |
Account changes (clients ask first; the server re-checks with Substack afterwards)
| Tool | What it does |
|---|---|
subscribe |
Free-subscribes you. Never starts a paid plan |
unsubscribe |
Removes a free subscription. Anything with a payment attached is refused |
Digest (only when SUBSTACK_DIGEST_DIR is set; plain-text output for agent harnesses)
| Tool | What it does |
|---|---|
digest_begin |
Fetches every new post and chat activity, and returns a work list with refs (P1, P2, …) |
digest_finish |
Takes a verdict per post and chat, renders the message and saves state. Safe to repeat |
digest_status · mark_reported |
Inspect the state · repair it |
save_interests_proposal |
Saves a drafted interests.md as interests.proposed.md, never over the real one |
A worked example of a whole run, with each tool's arguments and output: docs/digest-tools.md.
Hermes Agent runs skills on a schedule and delivers to Discord and other chats. hermes/SKILL.md turns this server into a morning digest:
- What it covers: every new post in your subscriptions, plus chats and DMs with new activity.
- How it reads: subagents read every post in full.
- What you get: one message, with:
- picks worth reading in full, each with a summary and a reason
- everything else, grouped by publication
- chats, with anything addressed to you first
- What the model does: only the judging. Fetching, retries, state and layout are code, so the whole digest can run on a local 27B model.
Setup
- Build (
npm ci && npm run build) and copydist/,package.jsonandpackage-lock.jsonto where Hermes can see them, e.g./opt/data/mcp/substack-reader-mcp. Then runnpm ci --omit=dev --omit=optionalthere. - Log in on a machine with a browser (
node dist/cli.js login), then copy~/.config/substack-reader/auth.jsonto the Hermes host, e.g./opt/data/mcp/substack-reader-home/. - Register the server in Hermes's
config.yaml:mcp_servers: substack-reader: command: node args: ["/opt/data/mcp/substack-reader-mcp/dist/cli.js"] env: SUBSTACK_READER_HOME: /opt/data/mcp/substack-reader-home SUBSTACK_DIGEST_DIR: /opt/data/sandbox/substack_digest # absolute; created if needed SUBSTACK_DIGEST_TZ: America/Denver # SUBSTACK_CLASSIFIER: jev # optional; see below # SUBSTACK_JEV_API_KEY: ${JEV_OPENROUTER_API_KEY} # from Hermes's .env # SUBSTACK_JEV_URL: https://openrouter.ai/api/alpha/decisions # the default; any decisions-API endpoint
- Install the skill: copy
hermes/SKILL.mdtoskills/productivity/substack-digest/andhermes/substack_digest_start.shtoscripts/. Then edit the skill's Settings block. Optionally add aninterests.mdto the digest directory (fromhermes/interests.example.md, or drafted from your activity). - Schedule it (run as the
hermesuser). The job needs only thedelegationtoolset and this server:hermes cron create "0 7 * * *" "Run the substack-digest skill and deliver the digest." \ --name substack-digest --skill substack-digest --script substack_digest_start.sh --deliver discord:<channel-id>
More detail is in docs/digest.md: why the server has harness-specific tools, customizing, state, rate limits, chats and cost.
Optional here, and off by default.
- Your Substack feed is only what you subscribe to, so it's already curated, and the digest can afford to read every post.
- This classifier was built for medium-reader-mcp, where the feed is effectively endless (100–200 new posts a day from writers you never chose), and was carried over here.
What it still adds on Substack:
- Jev is a "decision model" from TypeSafe. It returns typed answers with probabilities, not text. Before anything is read, it gives every post:
- a rank (how much you'd want it, judged against
interests.md) - a skip probability (whether it matches your Skip list)
- a rank (how much you'd want it, judged against
- In the digest:
- Posts are read best-first, and the rank informs the "Read in full" picks.
- Confident skips (say, podcast show notes) aren't read at all.
- With
SUBSTACK_DIGEST_RANK_FLOOR, low-ranked posts become optional, which helps if your subscription list grows.
- Backends (
SUBSTACK_CLASSIFIER):off(default).jev: ranks and skips. The server calls the decisions API itself over HTTPS, not through sampling.sampling: skips only. The server asks your MCP client's own model to rate each headline, which is what MCP sampling means: the server borrows the client's LLM rather than having its own. It doesn't rank, so ordering is left to the agent's model.
- Any Jev-style provider: the
jevbackend speaks the decisions API (POSTmodel,state,questions→answers), and you point it at a provider the way you'd point an OpenAI-compatible client at a local model:SUBSTACK_JEV_URL: the endpoint. Default: OpenRouter'shttps://openrouter.ai/api/alpha/decisions. TypeSafe's System One API (…/v1/systemone) and compatible or self-hosted servers work too.SUBSTACK_JEV_API_KEY: the bearer token (falls back toOPENROUTER_API_KEY). It can be empty for a server without auth.SUBSTACK_JEV_MODEL: the model id. Defaulttypesafe/jev-1.13; TypeSafe's own API usesjev-1.13.
- Tune it to you:
- Label your own posts with one keypress each, and the tools recommend thresholds and edits to
interests.md. - The tools can also draft an
interests.mdfrom what you pay for, save, heart, finish and dismiss.
- Label your own posts with one keypress each, and the tools recommend thresholds and edits to
Everything about it is in docs/classifier.md.
- Browser (default):
loginopens Chrome or Edge on Substack's sign-in page. The session is saved as soon as you're in and lasts about three months. Re-runningloginusually needs no typing. - Paste:
login --paste(or--stdin) takes thesubstack.sidcookie in any format: the bare value, aCookie:header, a Cookie-Editor export, orcookies.txt. - No restart needed: the server re-reads the session on every call.
- Tips:
- If Substack emails you a sign-in link, paste it into the
loginwindow. - Google sign-in sometimes refuses an automated window; use email or password instead.
- If Substack emails you a sign-in link, paste it into the
| Command | What it does |
|---|---|
node dist/cli.js login |
Log in (--paste / --stdin to paste the cookie) |
node dist/cli.js status |
The logged-in account, subscription count, and whether the session works |
node dist/cli.js logout |
Delete the session (--all also deletes the login browser profile) |
node dist/cli.js install |
Register with Claude Code (--scope user|project|local) |
Environment variables
| Variable | Purpose |
|---|---|
SUBSTACK_SID, SUBSTACK_COOKIE |
Session cookie, overriding the saved session (containers, CI) |
SUBSTACK_READER_HOME |
Config directory (default ~/.config/substack-reader) |
SUBSTACK_BROWSER_PATH |
A Chromium-based browser for login |
SUBSTACK_USERNAME |
When logged out, list_subscriptions shows this user's public subscriptions |
SUBSTACK_COOKIES_PATH |
A Cookie-Editor export from the original Python version, read as a fallback |
SUBSTACK_DIGEST_DIR |
Absolute path of the digest directory; setting it turns on the digest tools |
SUBSTACK_DIGEST_TZ |
Time zone for digest and chat times (default UTC) |
SUBSTACK_CLASSIFIER |
off (default), jev or sampling |
SUBSTACK_JEV_URL |
Decisions-API endpoint for jev (default OpenRouter's) |
SUBSTACK_JEV_API_KEY |
Bearer token for that endpoint (default OPENROUTER_API_KEY) |
SUBSTACK_JEV_MODEL |
Model id (default typesafe/jev-1.13) |
SUBSTACK_DIGEST_SKIP_THRESHOLD |
Skip probability at which a post is set aside (default 0.7) |
SUBSTACK_DIGEST_RANK_FLOOR |
Rank below which reading a post is optional (default off) |
- Your session goes only to
substack.comand*.substack.com. Custom-domain publications get their own session through Substack's sign-in handoff, as in a browser. Redirects are followed one hop at a time, so no cookie reaches another host. - It's stored in
~/.config/substack-reader/auth.json, mode600. Nothing is stored in the repo. - No telemetry. Requests go only to Substack, to the publications you ask about, and to your Jev provider if it's on (headlines and your interests only).
- Only
subscribeandunsubscribechange anything, and only free subscriptions. - Reading activity is read, never changed. The endpoints that mark posts as seen or saved are never called.
package-lock.jsonpins every dependency; install withnpm ci.
| Symptom | Fix |
|---|---|
| "Not logged in" / "session was rejected" | node dist/cli.js login (sessions last about three months) |
| "Only a preview was returned" | No paid subscription to that publication, or the session expired; status checks |
The server doesn't appear in /mcp |
Restart Claude Code |
login can't find a browser |
Install Chrome, set SUBSTACK_BROWSER_PATH, or use --paste |
| "doesn't look like a Substack publication" | The newsletter may have left Substack |
The digest says CLASSIFIER: … unavailable |
For jev, check SUBSTACK_JEV_URL and the key; for sampling, the client's sampling setup |
npm ci && npm test # vitest against a fake fetch; no network needed
npm run typecheck && npm run buildsrc/substack/: the HTTP client, posts, subscriptions, reader shelves and chat.src/digest/: the digest tools.src/classifier/: the headline classifier, shared with medium-reader-mcp.tools/classifier/: label, score, analyze, propose.- CLAUDE.md: how Substack's endpoints actually behave, including look-alikes that write. Read it before changing
src/substack/.
An independent project, not affiliated with or endorsed by Substack or TypeSafe. It uses Substack's undocumented web endpoints, which can change without notice. Use it with your own account, within Substack's Terms of Use.
MIT © 2026 Bill Baran.