Substack-OPS

Substack-OPS

Standalone Substack CLI + 26-tool MCP server. Your IDE drafts the replies. Zero AI API keys.

Category
访问服务器

README

substack-ops

<!-- mcp-name: io.github.06ketan/substack-ops -->

PyPI version Python 3.12+ License: MIT MCP compatible CI

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-grab

Auth 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 tour

Command 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
auth verify Confirm the cookie works; print authed user/pub.
auth test Same as verify, exit non-zero on failure (CI-friendly).
auth login --browser chrome|brave Auto-grab cookie from local Chromium browser via macOS Keychain.
auth login --email me@x.com Email magic-link → paste-the-link interactive flow.
auth setup Interactive paste of connect.sid cookie.

Read — Posts (8)

Command What it does
posts list [--pub] [--limit] [--sort new|top] List posts from a publication (yours by default).
posts show <id|slug> [--pub] Post metadata (title, dates, reactions, comment count).
posts get --slug <slug> [--pub] Same as show but slug-only.
posts content <id> [--md] [--pub] HTML body (auth-aware for paywalled). --md converts to Markdown.
posts stats <id> Engagement counts — reactions, comments.
posts search <query> [--pub] [--limit] Substack-side full-text search.
posts paywalled <id> [--pub] Boolean: is this post paywalled?
posts react <id> [--off] [--pub] Add (or remove with --off) a reaction. Defaults to ❤.
posts restack <id> [--off] Restack a post (Substack does not support unrestack).

Read — Notes (5)

Command What it does
notes list [--limit] Your published Notes.
notes show <id> One note + its reply tree.
notes publish <body> [--no-dry-run] Publish a top-level Note.
notes react <id> [--off] React on any Note.
notes restack <id> [--off] Restack a Note.

Read + Write — Comments (5)

Command What it does
comments tree <post_id> [--pub] Full nested comment tree as table.
comments export <post_id> --out file.json [--pub] Same tree as JSON.
comments add <post_id> <body> [--pub] [--no-dry-run] New top-level comment.
comments react <id> --kind post|note [--off] React on a comment.
comments delete <id> --kind post|note [--no-dry-run] Destructive — your own comments only.

Reply engine (6)

Command What it does
reply template <post_id> --template thanks Rule-based replies (no LLM).
reply review <post_id> LLM drafts each, you [a]ccept / [e]dit / [s]kip / [q]uit.
reply bulk <post_id> --out drafts.json Draft every comment to a file. Edit, set action: "approved".
reply note-bulk <note_id> --out drafts.json Same for replies under a Note.
reply bulk-send drafts.json [--no-dry-run] Posts only approved rows. Dedup-checked.
reply auto <post_id> --no-dry-run --yes-i-mean-it Draft + post immediately. 30s rate limit.

Read — Discovery (8)

Command What it does
feed list --tab for-you|subscribed|category-{slug} Reader feed (the Substack app feed).
profile me / profile get <handle> Profile.
users get <handle> / users subscriptions <handle> Public user info + their subs.
podcasts list [--pub] Audio posts.
recommendations list [--pub] Pub's recommended publications.
authors list [--pub] Pub's contributor list.
categories list / categories get --name <X> Substack's category taxonomy.

Automations (3)

Command What it does
auto presets List built-in YAML rules.
auto run <name> One-shot run a preset.
auto daemon <name> --interval 60 Loop forever; logs to audit.

Operations + safety (3)

Command What it does
audit search [--kind] [--target] [--status] [--since 7d] Query the JSONL audit log.
audit dedup-status Counts in the dedup SQLite DB.
quickstart 20-step interactive tour.

MCP server (3)

Command What it does
mcp install <cursor|claude-desktop|claude-code|print> [--dry-run] Auto-merge config into your host.
mcp serve stdio MCP server (26 tools).
mcp list-tools Print the tool registry.

Other (1)

Command What it does
tui 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 stratechery

Reply modes

Mode What it does Safety
template YAML keyword/regex rules under src/substack_ops/templates/*.yaml dry-run default
review LLM drafts each reply, you [a]ccept / [e]dit / [s]kip / [q]uit dry-run default + manual gate per comment
bulk LLM drafts every comment to drafts.json. Edit file, set action: "approved" offline review, dedup-checked on send
bulk-send Posts only items with action: "approved" dry-run default; dedup DB prevents the M2 31-dup-replies regression
auto LLM drafts and posts immediately requires --no-dry-run --yes-i-mean-it, 30s rate limit

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

  1. like-back — when someone reacts to your note, react to their latest note.
  2. auto-reply — same trigger, but post a templated thank-you.
  3. auto-restack — when a watchlist handle posts a new note, restack it.
  4. 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 tools

Manual 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 serve

MCP-native draft loop (no API key)

3 tools designed to let your host LLM draft for you:

Tool What it does
get_unanswered_comments Returns the worklist: comments where you have not yet replied (any depth).
propose_reply Dry-run only. Returns a token + payload preview. No write.
confirm_reply 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:

  1. MCP-native (default). Host LLM drafts via propose_reply / confirm_reply. No env vars, no API key. Use this for interactive replies.
  2. Subprocess CLI (daemon path). For reply auto / auto daemon when no human is in the loop. Auto-detects claude (Claude Code), cursor-agent, or codex on PATH. Override with SUBSTACK_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 tui

6 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 cookies

Architecture

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 GET https://substack.com/api/v1/subscriptions
List posts GET {pub}/api/v1/archive
Post by id GET {pub}/api/v1/posts/by-id/{id}
Post by slug GET {pub}/api/v1/posts/{slug}
Post content same as above; body_html field
Post search GET {pub}/api/v1/archive?search=
Comments GET {pub}/api/v1/post/{id}/comments?all_comments=true
Reply to comment POST {pub}/api/v1/post/{id}/comment body {body, parent_id}
Add top-level comment same with parent_id: null
React to post POST {pub}/api/v1/post/{id}/reaction body {reaction}
Restack post POST https://substack.com/api/v1/restack body {post_id}
Restack note POST https://substack.com/api/v1/restack body {comment_id}
Delete post-comment DELETE {pub}/api/v1/comment/{id} (PUB host)
Delete note DELETE https://substack.com/api/v1/comment/{id} (BARE host)
My notes GET https://substack.com/api/v1/reader/feed/profile/{user_id}
Note thread GET https://substack.com/api/v1/reader/comment/{note_id}
Note replies GET https://substack.com/api/v1/reader/comment/{note_id}/replies
Publish note POST https://substack.com/api/v1/comment/feed body {bodyJson}
Reply to note same with {bodyJson, parent_id} (NOT parent_comment_id — known M2 bug)
React to comment POST {host}/api/v1/comment/{id}/reaction (host = pub for post-comments, substack.com for notes)
Recommendations GET {pub}/api/v1/recommendations/from/{publication_id}
Authors GET {pub}/api/v1/publication/users/ranked?public=true
Categories GET https://substack.com/api/v1/categories
User profile GET https://substack.com/api/v1/user/{handle}/public_profile (auto-redirects on 404)
Reader feed GET https://substack.com/api/v1/reader/feed/{recommended|subscribed|category/{slug}}

Tests

uv run pytest -q     # 43 tests, ~0.6s, no live network

Coverage 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_from triggers are stubbed (return note: "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.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选