Skip to content
plugmcp
DirectorySkillsCategoriesBlog
EN/中文
Sign inSign up
Home/Servers/Substack Ops

Substack Ops

by io.github.06ketan

Substack CLI + 26-tool MCP server. Your IDE drafts replies via propose_reply. No API keys.

Power your agent

Add this server

The publisher's own command. Not a PlugMCP endpoint.

Local command
uvx substack-ops==0.3.5
Credentials from the publisher
  • SUBSTACK_PUBLICATION_URL — Your Substack publication URL (e.g. https://you.substack.com/). Optional if set in ~/.cursor/mcp.json's mcpServers.substack-api.env.
  • SUBSTACK_USER_ID — Your Substack numeric user id. Optional if set in ~/.cursor/mcp.json.
  • SUBSTACK_SESSION_TOKEN — Your Substack session cookie value (the s%3A... string). Required for authenticated calls. Easier path: run `substack-ops auth login --browser chrome` and the cookie is auto-grabbed.
  • SUBSTACK_OPS_MCP_PATH — Override path to the mcp.json file the auth layer reads. Defaults to ~/.cursor/mcp.json.
  • SUBSTACK_OPS_LLM_CMD — Optional: override the host CLI used by the unattended daemon path (default: auto-detect claude / cursor-agent / codex on PATH). Use {prompt} placeholder if your CLI takes the prompt as an arg.
Provider
Claude Code
SUBSTACK_PUBLICATION_URL=<value> SUBSTACK_USER_ID=<value> SUBSTACK_SESSION_TOKEN=<secret> SUBSTACK_OPS_MCP_PATH=<value> SUBSTACK_OPS_LLM_CMD=<value> claude mcp add io.github.06ketan-substack-ops -- uvx substack-ops==0.3.5

Run in a terminal.

Claude Desktop
{
  "mcpServers": {
    "io.github.06ketan-substack-ops": {
      "command": "uvx",
      "args": [
        "substack-ops==0.3.5"
      ],
      "env": {
        "SUBSTACK_PUBLICATION_URL": "<value>",
        "SUBSTACK_USER_ID": "<value>",
        "SUBSTACK_SESSION_TOKEN": "<secret>",
        "SUBSTACK_OPS_MCP_PATH": "<value>",
        "SUBSTACK_OPS_LLM_CMD": "<value>"
      }
    }
  }
}

claude_desktop_config.json

Step-by-step: add this in Claude

Remote URL or a command on your machine? How the two installs differ

Run on PlugMCP

Host a copy with our keys and logs.

A new server you create. It is not this listing's official endpoint. Use the publisher install when you want their URL and their credentials.

Launch

Related MCP

View more
  • Firecrawl🔥 Official Firecrawl MCP Server - Adds powerful web scraping and search to Cursor, Claude and any other LLM clients.
  • N
    NotionToken-auth Notion MCP: pages, databases, blocks, comments, files. Runs headless in CI and agents.
  • Context7Context7 Platform -- Up-to-date code documentation for LLMs and AI code editors

Related skills

View all
  • reaReverse engineer anything with agents, from app behavior down to native binaries.
  • rea-changelog-updatePrepare or rewrite REA release changelogs and GitHub release notes from pinned Git history, with verified contributor thanks and Release Please synchronization.
  • rea-tool-designDesign or change REA investigation tools, CLI/MCP contracts, provider capabilities, and Evidence semantics.

Substack CLI + 26-tool MCP server. Your IDE drafts replies via propose_reply. No API keys.

substack-ops

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.

Site → substack-ops.chavan.in · Source → 06ketan/substack-ops · Glama → mcp/servers/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, opencode, 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.

Wrong install?

This project is substack-ops on PyPI — install with uv / uvx, not unrelated npx packages that appear when searching “Substack MCP”. Canonical listing: Glama — 06ketan/substack-ops.

Works with (MCP)

These rows help discovery (search keywords); confirm each host’s current MCP docs before upgrading.

Open source–oriented hosts

HostDocumentationTypical wire-up
OpenCodeOpenCode MCP serversuvx substack-ops mcp install opencode
ContinueContinueuvx substack-ops mcp install print — paste the snippet into Continue’s MCP settings
ZedZedConfigure stdio MCP per Zed’s docs
ClineClineMCP setup per extension / marketplace docs
GooseGooseMCP extensions per Goose docs

Large commercial stacks

HostDocumentationTypical wire-up
CursorCursor MCPuvx substack-ops mcp install cursor
Claude (Desktop / Code)Claude Desktop, Claude Codemcp install claude-desktop / claude-code
GitHub CopilotCopilotMCP in VS Code / Copilot where supported — use print + host docs
ChatGPTOpenAIDeveloper / connector flows — often REST (Slideshot API) for tools without MCP
Google GeminiGeminiGemini CLI / IDE features per Google docs — stdio where supported

OpenCode (copy-paste)

Auto-install:

uvx substack-ops mcp install opencode

Manual (~/.config/opencode/opencode.json):

{
  "mcp": {
    "substack-ops": {
      "type": "local",
      "command": ["uvx", "substack-ops", "mcp", "serve"],
      "enabled": true
    }
  }
}

Optional version pin: use ["uvx", "substack-ops==0.3.5", "mcp", "serve"] (replace with current PyPI release).

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)

CommandWhat it does
auth verifyConfirm the cookie works; print authed user/pub.
auth testSame as verify, exit non-zero on failure (CI-friendly).
auth login --browser chrome|braveAuto-grab cookie from local Chromium browser via macOS Keychain.
auth login --email [email protected]Email magic-link → paste-the-link interactive flow.
auth setupInteractive paste of connect.sid cookie.

Read — Posts (8)

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

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

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

CommandWhat it does
reply template <post_id> --template thanksRule-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.jsonDraft every comment to a file. Edit, set action: "approved".
reply note-bulk <note_id> --out drafts.jsonSame 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-itDraft + post immediately. 30s rate limit.

Read — Discovery (8)

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

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

Operations + safety (3)

CommandWhat it does
audit search [--kind] [--target] [--status] [--since 7d]Query the JSONL audit log.
audit dedup-statusCounts in the dedup SQLite DB.
quickstart20-step interactive tour.

MCP server (3)

CommandWhat it does
mcp install <cursor|claude-desktop|claude-code|opencode|print> [--dry-run]Auto-merge config into your host.
mcp servestdio MCP server (26 tools).
mcp list-toolsPrint the tool registry.

Other (1)

CommandWhat it does
tuiTextual 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

ModeWhat it doesSafety
templateYAML keyword/regex rules under src/substack_ops/templates/*.yamldry-run default
reviewLLM drafts each reply, you [a]ccept / [e]dit / [s]kip / [q]uitdry-run default + manual gate per comment
bulkLLM drafts every comment to drafts.json. Edit file, set action: "approved"offline review, dedup-checked on send
bulk-sendPosts only items with action: "approved"dry-run default; dedup DB prevents the M2 31-dup-replies regression
autoLLM drafts and posts immediatelyrequires --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 opencode          # auto-add to ~/.config/opencode/opencode.json
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:

ToolWhat it does
get_unanswered_commentsReturns the worklist: comments where you have not yet replied (any depth).
propose_replyDry-run only. Returns a token + payload preview. No write.
confirm_replyPosts 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 [email protected]  # 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

ActionMethod + URL
Auth checkGET https://substack.com/api/v1/subscriptions
List postsGET {pub}/api/v1/archive
Post by idGET {pub}/api/v1/posts/by-id/{id}
Post by slugGET {pub}/api/v1/posts/{slug}
Post contentsame as above; body_html field
Post searchGET {pub}/api/v1/archive?search=
CommentsGET {pub}/api/v1/post/{id}/comments?all_comments=true
Reply to commentPOST {pub}/api/v1/post/{id}/comment body {body, parent_id}
Add top-level commentsame with parent_id: null
React to postPOST {pub}/api/v1/post/{id}/reaction body {reaction}
Restack postPOST https://substack.com/api/v1/restack body {post_id}
Restack notePOST https://substack.com/api/v1/restack body {comment_id}
Delete post-commentDELETE {pub}/api/v1/comment/{id} (PUB host)
Delete noteDELETE https://substack.com/api/v1/comment/{id} (BARE host)
My notesGET https://substack.com/api/v1/reader/feed/profile/{user_id}
Note threadGET https://substack.com/api/v1/reader/comment/{note_id}
Note repliesGET https://substack.com/api/v1/reader/comment/{note_id}/replies
Publish notePOST https://substack.com/api/v1/comment/feed body {bodyJson}
Reply to notesame with {bodyJson, parent_id} (NOT parent_comment_id — known M2 bug)
React to commentPOST {host}/api/v1/comment/{id}/reaction (host = pub for post-comments, substack.com for notes)
RecommendationsGET {pub}/api/v1/recommendations/from/{publication_id}
AuthorsGET {pub}/api/v1/publication/users/ranked?public=true
CategoriesGET https://substack.com/api/v1/categories
User profileGET https://substack.com/api/v1/user/{handle}/public_profile (auto-redirects on 404)
Reader feedGET https://substack.com/api/v1/reader/feed/{recommended|subscribed|category/{slug}}

Related MCPs

  • slideshot — HTML → slides (PNG / PDF / PPTX); npm slideshot-mcp.
  • medium-ops — Medium stories + responses + MCP (PyPI medium-ops).

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.

How do I install Substack Ops?
Copy the stdio config for Cursor, Claude Desktop, Claude Code, or VS Code on this page. It uses the publisher's install, not a PlugMCP proxy.
Does Substack Ops need an API key?
The publisher lists these environment variables: SUBSTACK_PUBLICATION_URL, SUBSTACK_USER_ID, SUBSTACK_SESSION_TOKEN, SUBSTACK_OPS_MCP_PATH, SUBSTACK_OPS_LLM_CMD.
What transport does Substack Ops use?
Substack Ops runs locally over stdio.
When was Substack Ops last checked?
Last checked 2026-10-16. Substack CLI + 26-tool MCP server. Your IDE drafts replies via propose_reply. No API keys.

No tool list yet. Remote servers get one after the first health check.

plugmcp

Plug any tool into any agent. MCP servers for people who do not write servers. Gateway for volume, n8n for apps.

  • github.com/TODO
  • x.com/TODO
  • linkedin.com/company/TODO

PlugMCP, 2026

Product
  • Browse
  • Install
  • Host your own
  • Voice mode
  • Browser
Directory
  • All MCP servers
  • Agent skills
  • Categories
  • New
  • Voice
  • Submit a server
Developers
  • Docs
  • Blog
  • What is MCP
  • What is WebMCP
  • Status
Company
  • About
  • Contact
  • Terms
  • Privacy
  • DPA