Substack-OPS
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