{
  "markdown": "# substack-ops\n\n<!-- mcp-name: io.github.06ketan/substack-ops -->\n\n[![PyPI version](https://img.shields.io/pypi/v/substack-ops?color=ff6719&label=pypi)](https://pypi.org/project/substack-ops/)\n[![PyPI downloads](https://img.shields.io/pypi/dm/substack-ops?color=ff6719&label=downloads)](https://pypi.org/project/substack-ops/)\n[![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-3776AB)](https://www.python.org/downloads/)\n[![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)\n[![MCP compatible](https://img.shields.io/badge/MCP-compatible-8A2BE2)](https://modelcontextprotocol.io)\n[![MCP Registry](https://img.shields.io/badge/MCP_Registry-verified-blue)](https://registry.modelcontextprotocol.io/v0/servers?search=io.github.06ketan/substack-ops)\n[![Anthropic DXT](https://img.shields.io/badge/Anthropic_DXT-.mcpb-D97706)](https://github.com/06ketan/substack-ops/releases/latest)\n[![Glama MCP server](https://glama.ai/mcp/servers/06ketan/substack-ops/badges/score.svg)](https://glama.ai/mcp/servers/06ketan/substack-ops)\n[![MCP Badge](https://lobehub.com/badge/mcp/06ketan-substack-ops)](https://lobehub.com/mcp/06ketan-substack-ops)\n[![CI](https://github.com/06ketan/substack-ops/actions/workflows/test.yml/badge.svg)](https://github.com/06ketan/substack-ops/actions/workflows/test.yml)\n\n> **Standalone Substack CLI + 26-tool MCP server for Cursor MCP, Claude MCP, OpenCode MCP, and any stdio MCP host. Your IDE drafts the replies. Zero AI API keys.**\n\nSite → **[substack-ops.chavan.in](https://substack-ops.chavan.in)** · Source → **[06ketan/substack-ops](https://github.com/06ketan/substack-ops)** · Glama → **[mcp/servers/06ketan/substack-ops](https://glama.ai/mcp/servers/06ketan/substack-ops)**\n\n<a href=\"https://glama.ai/mcp/servers/06ketan/substack-ops\">\n  <img width=\"380\" height=\"200\" src=\"https://glama.ai/mcp/servers/06ketan/substack-ops/badges/card.svg\" alt=\"substack-ops MCP server card on Glama\" />\n</a>\n\nPosts, notes, comments, replies, reactions, restacks, recommendations, search,\nprofiles, feeds, automations, MCP server, Textual TUI. One Python install, one\nbinary, MIT licensed.\n\n## TL;DR — MCP-native (no API key, one command)\n\n```bash\nuvx substack-ops mcp install cursor          # or claude-desktop, claude-code, opencode, print\n# Restart your host. Then in chat:\n#   \"list unanswered comments on post 193866852\"\n#   \"draft a warm reply to comment 12345\"\n#   \"post that draft\"\n```\n\nYour **host's** LLM (Cursor's, Claude's) does the drafting via the\n`propose_reply` / `confirm_reply` tools. No `ANTHROPIC_API_KEY` /\n`OPENAI_API_KEY` needed.\n\n### Wrong install?\n\nThis project is **`substack-ops` on [PyPI](https://pypi.org/project/substack-ops/)** — install with **`uv` / `uvx`**, not unrelated **`npx`** packages that appear when searching “Substack MCP”. Canonical listing: **[Glama — 06ketan/substack-ops](https://glama.ai/mcp/servers/06ketan/substack-ops)**.\n\n## Works with (MCP)\n\nThese rows help discovery (search keywords); **confirm each host’s current MCP docs** before upgrading.\n\n### Open source–oriented hosts\n\n| Host | Documentation | Typical wire-up |\n|------|---------------|-----------------|\n| **OpenCode** | [OpenCode MCP servers](https://open-code.ai/en/docs/mcp-servers) | `uvx substack-ops mcp install opencode` |\n| **Continue** | [Continue](https://docs.continue.dev) | `uvx substack-ops mcp install print` — paste the snippet into Continue’s MCP settings |\n| **Zed** | [Zed](https://zed.dev/docs) | Configure stdio MCP per Zed’s docs |\n| **Cline** | [Cline](https://github.com/cline/cline) | MCP setup per extension / marketplace docs |\n| **Goose** | [Goose](https://block.github.io/goose/) | MCP extensions per Goose docs |\n\n### Large commercial stacks\n\n| Host | Documentation | Typical wire-up |\n|------|---------------|-----------------|\n| **Cursor** | [Cursor MCP](https://docs.cursor.com/context/model-context-protocol) | `uvx substack-ops mcp install cursor` |\n| **Claude** (Desktop / Code) | [Claude Desktop](https://support.anthropic.com/en/articles/10065433-installing-claude-for-desktop), [Claude Code](https://docs.claude.com/en/docs/claude-code) | `mcp install claude-desktop` / `claude-code` |\n| **GitHub Copilot** | [Copilot](https://docs.github.com/en/copilot) | MCP in VS Code / Copilot where supported — use `print` + host docs |\n| **ChatGPT** | [OpenAI](https://platform.openai.com/docs) | Developer / connector flows — often REST ([Slideshot API](https://slideshot.vercel.app)) for tools without MCP |\n| **Google Gemini** | [Gemini](https://ai.google.dev/docs) | Gemini CLI / IDE features per Google docs — stdio where supported |\n\n### OpenCode (copy-paste)\n\nAuto-install:\n\n```bash\nuvx substack-ops mcp install opencode\n```\n\nManual (`~/.config/opencode/opencode.json`):\n\n```json\n{\n  \"mcp\": {\n    \"substack-ops\": {\n      \"type\": \"local\",\n      \"command\": [\"uvx\", \"substack-ops\", \"mcp\", \"serve\"],\n      \"enabled\": true\n    }\n  }\n}\n```\n\nOptional version pin: use `[\"uvx\", \"substack-ops==0.3.5\", \"mcp\", \"serve\"]` (replace with current PyPI release).\n\n## Setup (dev / from source)\n\n```bash\ngit clone https://github.com/06ketan/substack-ops && cd substack-ops\nuv sync\nuv sync --extra mcp     # mcp SDK for the MCP server (recommended)\nuv sync --extra tui     # textual for the TUI\nuv sync --extra chrome  # pycryptodome + keyring for Chrome cookie auto-grab\n```\n\nAuth defaults to `~/.cursor/mcp.json`'s `mcpServers.substack-api.env`. Override\nwith env or `.env`. Or use one of the auth flows in `auth login` / `auth setup`.\n\n```bash\nuv run substack-ops auth verify\nuv run substack-ops quickstart   # 20-step tour\n```\n\n## Command surface\n\nGrouped by intent. Every write defaults to `--dry-run`; flip with\n`--no-dry-run` (and `--yes-i-mean-it` for the irreversible ones). All writes\nland in `.cache/audit.jsonl` and are dedup-checked against `.cache/actions.db`.\n\n### Auth (4)\n\n| Command | What it does |\n|---|---|\n| `auth verify` | Confirm the cookie works; print authed user/pub. |\n| `auth test` | Same as verify, exit non-zero on failure (CI-friendly). |\n| `auth login --browser chrome\\|brave` | Auto-grab cookie from local Chromium browser via macOS Keychain. |\n| `auth login --email me@x.com` | Email magic-link → paste-the-link interactive flow. |\n| `auth setup` | Interactive paste of `connect.sid` cookie. |\n\n### Read — Posts (8)\n\n| Command | What it does |\n|---|---|\n| `posts list [--pub] [--limit] [--sort new\\|top]` | List posts from a publication (yours by default). |\n| `posts show <id\\|slug> [--pub]` | Post metadata (title, dates, reactions, comment count). |\n| `posts get --slug <slug> [--pub]` | Same as `show` but slug-only. |\n| `posts content <id> [--md] [--pub]` | HTML body (auth-aware for paywalled). `--md` converts to Markdown. |\n| `posts stats <id>` | Engagement counts — reactions, comments. |\n| `posts search <query> [--pub] [--limit]` | Substack-side full-text search. |\n| `posts paywalled <id> [--pub]` | Boolean: is this post paywalled? |\n| `posts react <id> [--off] [--pub]` | Add (or remove with `--off`) a reaction. Defaults to ❤. |\n| `posts restack <id> [--off]` | Restack a post (Substack does not support unrestack). |\n\n### Read — Notes (5)\n\n| Command | What it does |\n|---|---|\n| `notes list [--limit]` | Your published Notes. |\n| `notes show <id>` | One note + its reply tree. |\n| `notes publish <body> [--no-dry-run]` | Publish a top-level Note. |\n| `notes react <id> [--off]` | React on any Note. |\n| `notes restack <id> [--off]` | Restack a Note. |\n\n### Read + Write — Comments (5)\n\n| Command | What it does |\n|---|---|\n| `comments tree <post_id> [--pub]` | Full nested comment tree as table. |\n| `comments export <post_id> --out file.json [--pub]` | Same tree as JSON. |\n| `comments add <post_id> <body> [--pub] [--no-dry-run]` | New top-level comment. |\n| `comments react <id> --kind post\\|note [--off]` | React on a comment. |\n| `comments delete <id> --kind post\\|note [--no-dry-run]` | Destructive — your own comments only. |\n\n### Reply engine (6)\n\n| Command | What it does |\n|---|---|\n| `reply template <post_id> --template thanks` | Rule-based replies (no LLM). |\n| `reply review <post_id>` | LLM drafts each, you `[a]ccept / [e]dit / [s]kip / [q]uit`. |\n| `reply bulk <post_id> --out drafts.json` | Draft every comment to a file. Edit, set `action: \"approved\"`. |\n| `reply note-bulk <note_id> --out drafts.json` | Same for replies under a Note. |\n| `reply bulk-send drafts.json [--no-dry-run]` | Posts only `approved` rows. Dedup-checked. |\n| `reply auto <post_id> --no-dry-run --yes-i-mean-it` | Draft + post immediately. 30s rate limit. |\n\n### Read — Discovery (8)\n\n| Command | What it does |\n|---|---|\n| `feed list --tab for-you\\|subscribed\\|category-{slug}` | Reader feed (the Substack app feed). |\n| `profile me` / `profile get <handle>` | Profile. |\n| `users get <handle>` / `users subscriptions <handle>` | Public user info + their subs. |\n| `podcasts list [--pub]` | Audio posts. |\n| `recommendations list [--pub]` | Pub's recommended publications. |\n| `authors list [--pub]` | Pub's contributor list. |\n| `categories list` / `categories get --name <X>` | Substack's category taxonomy. |\n\n### Automations (3)\n\n| Command | What it does |\n|---|---|\n| `auto presets` | List built-in YAML rules. |\n| `auto run <name>` | One-shot run a preset. |\n| `auto daemon <name> --interval 60` | Loop forever; logs to audit. |\n\n### Operations + safety (3)\n\n| Command | What it does |\n|---|---|\n| `audit search [--kind] [--target] [--status] [--since 7d]` | Query the JSONL audit log. |\n| `audit dedup-status` | Counts in the dedup SQLite DB. |\n| `quickstart` | 20-step interactive tour. |\n\n### MCP server (3)\n\n| Command | What it does |\n|---|---|\n| `mcp install <cursor\\|claude-desktop\\|claude-code\\|opencode\\|print> [--dry-run]` | Auto-merge config into your host. |\n| `mcp serve` | stdio MCP server (26 tools). |\n| `mcp list-tools` | Print the tool registry. |\n\n### Other (1)\n\n| Command | What it does |\n|---|---|\n| `tui` | Textual TUI — 6 tabs (Notes, Posts, Comments, Feed, Auto, Profile). |\n\n## Multi-publication\n\nEvery read command accepts `--pub <subdomain|domain>`. Defaults to your own\npublication.\n\n```bash\nsubstack-ops posts list --pub stratechery --limit 5\nsubstack-ops posts search \"ai\" --pub stratechery\nsubstack-ops recommendations list --pub stratechery\n```\n\n## Reply modes\n\n| Mode | What it does | Safety |\n|------|--------------|--------|\n| `template` | YAML keyword/regex rules under `src/substack_ops/templates/*.yaml` | dry-run default |\n| `review` | LLM drafts each reply, you `[a]ccept / [e]dit / [s]kip / [q]uit` | dry-run default + manual gate per comment |\n| `bulk` | LLM drafts every comment to `drafts.json`. Edit file, set `action: \"approved\"` | offline review, dedup-checked on send |\n| `bulk-send` | Posts only items with `action: \"approved\"` | dry-run default; **dedup DB prevents the M2 31-dup-replies regression** |\n| `auto` | LLM drafts and posts immediately | requires `--no-dry-run --yes-i-mean-it`, 30s rate limit |\n\nAfter every live note-reply the engine re-fetches the new comment and asserts\n`ancestor_path` is non-empty. If empty, the audit row's `result_status` is\nflipped to `\"orphaned\"` (the M2 bug where `parent_comment_id` was silently\ndropped — now caught).\n\n## Automations\n\nBuilt-in presets (`auto presets`):\n\n1. **like-back** — when someone reacts to your note, react to their latest note.\n2. **auto-reply** — same trigger, but post a templated thank-you.\n3. **auto-restack** — when a watchlist handle posts a new note, restack it.\n4. **follow-back** — when someone follows you, follow them back.\n\nCustom YAML rules under `~/.config/substack-ops/auto/*.yaml`. Loop with\n`auto daemon <name> --interval 60`.\n\n## MCP server\n\n```bash\nsubstack-ops mcp install opencode          # auto-add to ~/.config/opencode/opencode.json\nsubstack-ops mcp install cursor              # auto-add to ~/.cursor/mcp.json\nsubstack-ops mcp install claude-desktop      # auto-add to claude_desktop_config.json\nsubstack-ops mcp install claude-code         # uses `claude mcp add` under the hood\nsubstack-ops mcp install print               # print the snippet only\nsubstack-ops mcp install cursor --dry-run    # preview without writing\nsubstack-ops mcp serve                       # stdio server\nsubstack-ops mcp list-tools                  # 26 tools\n```\n\nManual config snippet (if you prefer):\n\n```json\n{\n  \"mcpServers\": {\n    \"substack-ops\": {\n      \"command\": \"substack-ops\",\n      \"args\": [\"mcp\", \"serve\"]\n    }\n  }\n}\n```\n\nIf the `mcp` SDK is not installed, the server falls back to a minimal\n`stdin/stdout` JSON-line dispatcher that's still useful for scripting:\n\n```bash\necho '{\"tool\":\"list_posts\",\"args\":{\"limit\":3}}' | substack-ops mcp serve\n```\n\n### MCP-native draft loop (no API key)\n\n3 tools designed to let your **host** LLM draft for you:\n\n| Tool | What it does |\n|------|--------------|\n| `get_unanswered_comments` | Returns the worklist: comments where you have not yet replied (any depth). |\n| `propose_reply` | Dry-run only. Returns a `token` + payload preview. **No write.** |\n| `confirm_reply` | Posts a previously-proposed reply by token. Idempotent via dedup DB. Token TTL 5 min. |\n\n**Differentiator tools** (the safety + drafting stack that makes the unattended\nmode safe): `bulk_draft_replies`, `send_approved_drafts`, `audit_search`,\n`dedup_status`, `get_unanswered_comments`, `propose_reply`, `confirm_reply`.\n\n## LLM strategy\n\nTwo layers, both free:\n\n1. **MCP-native (default).** Host LLM drafts via `propose_reply` /\n   `confirm_reply`. No env vars, no API key. Use this for interactive replies.\n2. **Subprocess CLI (daemon path).** For `reply auto` / `auto daemon` when\n   no human is in the loop. Auto-detects `claude` (Claude Code),\n   `cursor-agent`, or `codex` on PATH. Override with `SUBSTACK_OPS_LLM_CMD`.\n\nThere is no paid-API-key path. If you want one, vendor the old `_anthropic` /\n`_openai` methods from `substack-ops` v0.2.0 yourself.\n\n## Textual TUI\n\n```bash\nsubstack-ops tui\n```\n\n6 tabs: Notes / Posts / Comments / Feed / Auto / Profile.\nSub-tabs: 1=mine, 2=following, 3=general.\nKeys: tab, 1-3, ↑/↓, enter, r, l, s, o, q/esc.\n\n## Auth methods\n\n```bash\nsubstack-ops auth verify                  # uses mcp.json or env\nsubstack-ops auth login                   # auto-grab cookies from Chrome (macOS Keychain)\nsubstack-ops auth login --browser brave\nsubstack-ops auth login --email me@x.com  # email magic-link, paste-the-link mode\nsubstack-ops auth setup                   # interactive paste cookies\n```\n\n## Architecture\n\n```text\nmcp.json | env | Chrome | OTP  →  auth.py / auth_chrome.py / auth_otp.py\n                                            │\n                                  .cache/cookies.json\n                                            │\n                                  SubstackClient (httpx)\n                                            │\n   ┌──────┬──────┬───────┬───────┬───────┬──────┬──────┬─────┬──────┐\n   ▼      ▼      ▼       ▼       ▼       ▼      ▼      ▼     ▼      ▼\n posts  notes  comments  feed  profile  users  recs  cats  ...   reply_engine\n                                                                       │\n                                                       ┌───────────────┼────────────┐\n                                                       ▼               ▼            ▼\n                                                  template       ai_review     ai_bulk + ai_auto\n                                                       └───────────────┬────────────┘\n                                                                       ▼\n                                                            base.post_reply / post_note_reply\n                                                                       │\n                                                              ┌────────┼────────┐\n                                                              ▼        ▼        ▼\n                                                            dedup    audit  ancestor_path\n                                                            (SQLite) (jsonl)  guardrail\n   auto/engine.py ────────────────┐\n   mcp/server.py  ──── 23 tools ──┼─── all share SubstackClient\n   tui/app.py     ──── 6 tabs   ──┘\n```\n\n## Endpoints used\n\n| Action | Method + URL |\n|--------|--------------|\n| Auth check | `GET https://substack.com/api/v1/subscriptions` |\n| List posts | `GET {pub}/api/v1/archive` |\n| Post by id | `GET {pub}/api/v1/posts/by-id/{id}` |\n| Post by slug | `GET {pub}/api/v1/posts/{slug}` |\n| Post content | same as above; `body_html` field |\n| Post search | `GET {pub}/api/v1/archive?search=` |\n| Comments | `GET {pub}/api/v1/post/{id}/comments?all_comments=true` |\n| Reply to comment | `POST {pub}/api/v1/post/{id}/comment` body `{body, parent_id}` |\n| Add top-level comment | same with `parent_id: null` |\n| React to post | `POST {pub}/api/v1/post/{id}/reaction` body `{reaction}` |\n| Restack post | `POST https://substack.com/api/v1/restack` body `{post_id}` |\n| Restack note | `POST https://substack.com/api/v1/restack` body `{comment_id}` |\n| Delete post-comment | `DELETE {pub}/api/v1/comment/{id}` (PUB host) |\n| Delete note | `DELETE https://substack.com/api/v1/comment/{id}` (BARE host) |\n| My notes | `GET https://substack.com/api/v1/reader/feed/profile/{user_id}` |\n| Note thread | `GET https://substack.com/api/v1/reader/comment/{note_id}` |\n| Note replies | `GET https://substack.com/api/v1/reader/comment/{note_id}/replies` |\n| Publish note | `POST https://substack.com/api/v1/comment/feed` body `{bodyJson}` |\n| Reply to note | same with `{bodyJson, parent_id}` (NOT `parent_comment_id` — known M2 bug) |\n| React to comment | `POST {host}/api/v1/comment/{id}/reaction` (host = pub for post-comments, substack.com for notes) |\n| Recommendations | `GET {pub}/api/v1/recommendations/from/{publication_id}` |\n| Authors | `GET {pub}/api/v1/publication/users/ranked?public=true` |\n| Categories | `GET https://substack.com/api/v1/categories` |\n| User profile | `GET https://substack.com/api/v1/user/{handle}/public_profile` (auto-redirects on 404) |\n| Reader feed | `GET https://substack.com/api/v1/reader/feed/{recommended\\|subscribed\\|category/{slug}}` |\n\n## Related MCPs\n\n- **[slideshot](https://github.com/06ketan/slideshot)** — HTML → slides (PNG / PDF / PPTX); npm [`slideshot-mcp`](https://www.npmjs.com/package/slideshot-mcp).\n- **[medium-ops](https://github.com/06ketan/medium-ops)** — Medium stories + responses + MCP (PyPI `medium-ops`).\n\n## Tests\n\n```bash\nuv run pytest -q     # 43 tests, ~0.6s, no live network\n```\n\nCoverage today: auth, client (read+write+engagement+delete), reply engine,\ndedup DB, audit log search, MCP tool registry & dispatcher, automation engine\npreset loader, the M2 `parent_id` regression test, the M2 host-mismatch\nregression test.\n\n## GSD workflow\n\n`.planning/` scaffold for [Get Shit Done](https://github.com/khromov/get-shit-done)\nunder `~/.claude/skills/gsd-*`. Roadmap at `.planning/ROADMAP.md`,\nper-phase plans at `.planning/phases/M*/PHASE.md`.\n\n## Known gaps\n\n- Full email stats (opens/clicks/views) — needs dashboard CSRF flow. Fallback: Playwright MCP scrape.\n- Reactions endpoint shape on POST/DELETE not yet probed live; current shape is a best-guess from upstream tool catalogs.\n- Auto-engine `new_follower` / `new_note_from` triggers are stubbed (return `note: \"trigger not yet implemented\"`).\n- TUI sub-tabs (1/2/3) and reply/like/restack key bindings are scaffolded but not wired to the client yet.\n- Chrome cookie auto-grab tested only for macOS Chrome; Brave path included; Linux/Windows not supported.\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n\nThe vendored httpx-port helpers under `src/substack_ops/_substack/` are derived\nfrom the MIT-licensed `NHagar/substack_api` package — kept here so this repo\nships zero runtime dependencies on third-party Substack libraries. Attribution\npreserved in each file's module docstring.\n",
  "bytes": 19897,
  "sha": "6ea3d19253012c7db4709c34f6ea7b894e9a1eb4caae66c9bd5f802fbf9952e9",
  "repo_slug": "06ketan/substack-ops",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_06ketan_substack_ops_ea44b27f/readme"
}