Substack-OPS
Supports authentication via cookie auto-grab from local Brave browser (Chromium-based) for accessing Substack accounts through browser session data.
Provides comprehensive tools for interacting with Substack's platform, including managing posts, notes, comments, replies, reactions, restacks, recommendations, profiles, feeds, and automations. Enables AI-assisted drafting and publishing of content through MCP-native tools.
substack-ops
Standalone Substack CLI + 26-tool MCP server. Your IDE drafts the replies. Zero AI API keys.
Site → substack-ops.chavan.in · Source → 06ketan/substack-ops
Posts, notes, comments, replies, reactions, restacks, recommendations, search, profiles, feeds, automations, MCP server, Textual TUI. One Python install, one binary, MIT licensed.
TL;DR — MCP-native (no API key, one command)
uvx substack-ops mcp install cursor # or claude-desktop, claude-code, print
# Restart your host. Then in chat:
# "list unanswered comments on post 193866852"
# "draft a warm reply to comment 12345"
# "post that draft"Your host's LLM (Cursor's, Claude's) does the drafting via the
propose_reply / confirm_reply tools. No ANTHROPIC_API_KEY /
OPENAI_API_KEY needed.
Setup (dev / from source)
git clone https://github.com/06ketan/substack-ops && cd substack-ops
uv sync
uv sync --extra mcp # mcp SDK for the MCP server (recommended)
uv sync --extra tui # textual for the TUI
uv sync --extra chrome # pycryptodome + keyring for Chrome cookie auto-grabAuth defaults to ~/.cursor/mcp.json's mcpServers.substack-api.env. Override
with env or .env. Or use one of the auth flows in auth login / auth setup.
uv run substack-ops auth verify
uv run substack-ops quickstart # 20-step tourCommand surface
Grouped by intent. Every write defaults to --dry-run; flip with
--no-dry-run (and --yes-i-mean-it for the irreversible ones). All writes
land in .cache/audit.jsonl and are dedup-checked against .cache/actions.db.
Auth (4)
Command | What it does |
| Confirm the cookie works; print authed user/pub. |
| Same as verify, exit non-zero on failure (CI-friendly). |
| Auto-grab cookie from local Chromium browser via macOS Keychain. |
| Email magic-link → paste-the-link interactive flow. |
| Interactive paste of |
Read — Posts (8)
Command | What it does |
| List posts from a publication (yours by default). |
| Post metadata (title, dates, reactions, comment count). |
| Same as |
| HTML body (auth-aware for paywalled). |
| Engagement counts — reactions, comments. |
| Substack-side full-text search. |
| Boolean: is this post paywalled? |
| Add (or remove with |
| Restack a post (Substack does not support unrestack). |
Read — Notes (5)
Command | What it does |
| Your published Notes. |
| One note + its reply tree. |
| Publish a top-level Note. |
| React on any Note. |
| Restack a Note. |
Read + Write — Comments (5)
Command | What it does |
| Full nested comment tree as table. |
| Same tree as JSON. |
| New top-level comment. |
| React on a comment. |
| Destructive — your own comments only. |
Reply engine (6)
Command | What it does |
| Rule-based replies (no LLM). |
| LLM drafts each, you |
| Draft every comment to a file. Edit, set |
| Same for replies under a Note. |
| Posts only |
| Draft + post immediately. 30s rate limit. |
Read — Discovery (8)
Command | What it does |
| Reader feed (the Substack app feed). |
| Profile. |
| Public user info + their subs. |
| Audio posts. |
| Pub's recommended publications. |
| Pub's contributor list. |
| Substack's category taxonomy. |
Automations (3)
Command | What it does |
| List built-in YAML rules. |
| One-shot run a preset. |
| Loop forever; logs to audit. |
Operations + safety (3)
Command | What it does |
| Query the JSONL audit log. |
| Counts in the dedup SQLite DB. |
| 20-step interactive tour. |
MCP server (3)
Command | What it does |
| Auto-merge config into your host. |
| stdio MCP server (26 tools). |
| Print the tool registry. |
Other (1)
Command | What it does |
| Textual TUI — 6 tabs (Notes, Posts, Comments, Feed, Auto, Profile). |
Multi-publication
Every read command accepts --pub <subdomain|domain>. Defaults to your own
publication.
substack-ops posts list --pub stratechery --limit 5
substack-ops posts search "ai" --pub stratechery
substack-ops recommendations list --pub stratecheryReply modes
Mode | What it does | Safety |
| YAML keyword/regex rules under | dry-run default |
| LLM drafts each reply, you | dry-run default + manual gate per comment |
| LLM drafts every comment to | offline review, dedup-checked on send |
| Posts only items with | dry-run default; dedup DB prevents the M2 31-dup-replies regression |
| LLM drafts and posts immediately | requires |
After every live note-reply the engine re-fetches the new comment and asserts
ancestor_path is non-empty. If empty, the audit row's result_status is
flipped to "orphaned" (the M2 bug where parent_comment_id was silently
dropped — now caught).
Automations
Built-in presets (auto presets):
like-back — when someone reacts to your note, react to their latest note.
auto-reply — same trigger, but post a templated thank-you.
auto-restack — when a watchlist handle posts a new note, restack it.
follow-back — when someone follows you, follow them back.
Custom YAML rules under ~/.config/substack-ops/auto/*.yaml. Loop with
auto daemon <name> --interval 60.
MCP server
substack-ops mcp install cursor # auto-add to ~/.cursor/mcp.json
substack-ops mcp install claude-desktop # auto-add to claude_desktop_config.json
substack-ops mcp install claude-code # uses `claude mcp add` under the hood
substack-ops mcp install print # print the snippet only
substack-ops mcp install cursor --dry-run # preview without writing
substack-ops mcp serve # stdio server
substack-ops mcp list-tools # 26 toolsManual config snippet (if you prefer):
{
"mcpServers": {
"substack-ops": {
"command": "substack-ops",
"args": ["mcp", "serve"]
}
}
}If the mcp SDK is not installed, the server falls back to a minimal
stdin/stdout JSON-line dispatcher that's still useful for scripting:
echo '{"tool":"list_posts","args":{"limit":3}}' | substack-ops mcp serveMCP-native draft loop (no API key)
3 tools designed to let your host LLM draft for you:
Tool | What it does |
| Returns the worklist: comments where you have not yet replied (any depth). |
| Dry-run only. Returns a |
| Posts a previously-proposed reply by token. Idempotent via dedup DB. Token TTL 5 min. |
Differentiator tools (the safety + drafting stack that makes the unattended
mode safe): bulk_draft_replies, send_approved_drafts, audit_search,
dedup_status, get_unanswered_comments, propose_reply, confirm_reply.
LLM strategy
Two layers, both free:
MCP-native (default). Host LLM drafts via
propose_reply/confirm_reply. No env vars, no API key. Use this for interactive replies.Subprocess CLI (daemon path). For
reply auto/auto daemonwhen no human is in the loop. Auto-detectsclaude(Claude Code),cursor-agent, orcodexon PATH. Override withSUBSTACK_OPS_LLM_CMD.
There is no paid-API-key path. If you want one, vendor the old _anthropic /
_openai methods from substack-ops v0.2.0 yourself.
Textual TUI
substack-ops tui6 tabs: Notes / Posts / Comments / Feed / Auto / Profile. Sub-tabs: 1=mine, 2=following, 3=general. Keys: tab, 1-3, ↑/↓, enter, r, l, s, o, q/esc.
Auth methods
substack-ops auth verify # uses mcp.json or env
substack-ops auth login # auto-grab cookies from Chrome (macOS Keychain)
substack-ops auth login --browser brave
substack-ops auth login --email me@x.com # email magic-link, paste-the-link mode
substack-ops auth setup # interactive paste cookiesArchitecture
mcp.json | env | Chrome | OTP → auth.py / auth_chrome.py / auth_otp.py
│
.cache/cookies.json
│
SubstackClient (httpx)
│
┌──────┬──────┬───────┬───────┬───────┬──────┬──────┬─────┬──────┐
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
posts notes comments feed profile users recs cats ... reply_engine
│
┌───────────────┼────────────┐
▼ ▼ ▼
template ai_review ai_bulk + ai_auto
└───────────────┬────────────┘
▼
base.post_reply / post_note_reply
│
┌────────┼────────┐
▼ ▼ ▼
dedup audit ancestor_path
(SQLite) (jsonl) guardrail
auto/engine.py ────────────────┐
mcp/server.py ──── 23 tools ──┼─── all share SubstackClient
tui/app.py ──── 6 tabs ──┘Endpoints used
Action | Method + URL |
Auth check |
|
List posts |
|
Post by id |
|
Post by slug |
|
Post content | same as above; |
Post search |
|
Comments |
|
Reply to comment |
|
Add top-level comment | same with |
React to post |
|
Restack post |
|
Restack note |
|
Delete post-comment |
|
Delete note |
|
My notes |
|
Note thread |
|
Note replies |
|
Publish note |
|
Reply to note | same with |
React to comment |
|
Recommendations |
|
Authors |
|
Categories |
|
User profile |
|
Reader feed |
|
Tests
uv run pytest -q # 43 tests, ~0.6s, no live networkCoverage today: auth, client (read+write+engagement+delete), reply engine,
dedup DB, audit log search, MCP tool registry & dispatcher, automation engine
preset loader, the M2 parent_id regression test, the M2 host-mismatch
regression test.
GSD workflow
.planning/ scaffold for Get Shit Done
under ~/.claude/skills/gsd-*. Roadmap at .planning/ROADMAP.md,
per-phase plans at .planning/phases/M*/PHASE.md.
Known gaps
Full email stats (opens/clicks/views) — needs dashboard CSRF flow. Fallback: Playwright MCP scrape.
Reactions endpoint shape on POST/DELETE not yet probed live; current shape is a best-guess from upstream tool catalogs.
Auto-engine
new_follower/new_note_fromtriggers are stubbed (returnnote: "trigger not yet implemented").TUI sub-tabs (1/2/3) and reply/like/restack key bindings are scaffolded but not wired to the client yet.
Chrome cookie auto-grab tested only for macOS Chrome; Brave path included; Linux/Windows not supported.
License
MIT. See LICENSE.
The vendored httpx-port helpers under src/substack_ops/_substack/ are derived
from the MIT-licensed NHagar/substack_api package — kept here so this repo
ships zero runtime dependencies on third-party Substack libraries. Attribution
preserved in each file's module docstring.
Latest Blog Posts
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/06ketan/substack-ops'
If you have feedback or need assistance with the MCP directory API, please join our Discord server