Substack-OPS
Server Configuration
Describes the environment variables required to run the server.
| Name | Required | Description | Default |
|---|---|---|---|
| SUBSTACK_USER_ID | No | Your Substack numeric user id | |
| SUBSTACK_OPS_LLM_CMD | No | Override LLM command for daemon mode | |
| SUBSTACK_SESSION_TOKEN | No | Substack session cookie | |
| SUBSTACK_PUBLICATION_URL | No | Your Substack publication URL |
Capabilities
Features and capabilities supported by this server
| Capability | Details |
|---|---|
| tools | {
"listChanged": false
} |
| prompts | {
"listChanged": false
} |
| resources | {
"subscribe": false,
"listChanged": false
} |
| experimental | {} |
Tools
Functions exposed to the LLM to take actions
| Name | Description |
|---|---|
| test_connectionA | Read-only. Verify the Substack session cookie works and return the authenticated user's id, handle, and primary publication. Call this first if other tools 401 or to confirm setup. No args. |
| get_own_profileA | Read-only. Return the authenticated user's full profile as a dict with keys: id, handle, name, bio, photo_url, subscriber_count, primary_publication. Use this for 'who am I'-style calls and for preflight checks before WRITE tools (the auth handle is needed to build the publish URL). For another user's profile by handle, call get_profile instead. No args. |
| get_profileA | Read-only. Return any Substack user's public profile by their handle (the @-name from their URL, e.g. 'paulgraham' for paulgraham.substack.com). Returns id, handle, name, bio, photo_url, subscriber_count, and primary_publication. For YOUR own profile, prefer get_own_profile (faster, no handle needed, includes private fields). To list a user's posts after this, use list_posts with their pub url. |
| list_postsA | Read-only. List posts from a publication (yours by default). For a single post by id/slug use get_post; for full HTML body use get_post_content; to find by keyword use search_posts. |
| get_postA | Read-only. Return one post's metadata (title, slug, dates, reactions, comment count) by numeric id OR slug. For HTML body use get_post_content. For id-only callers prefer get_post_by_id. |
| get_post_by_idA | Read-only. Strict-typed variant of get_post that ONLY accepts a numeric post id (e.g. 193866852) — no slug fallback. Use this when your caller already has an integer id (e.g. from list_posts response) and you want type safety + fewer round-trips. Returns the same shape as get_post (title, slug, dates, reactions, comment_count). For a slug-or-id input use get_post; for the post body use get_post_content. |
| get_post_contentA | Read-only. Return a post's body. Auth-aware: returns full text for paywalled posts you have access to, otherwise only the free preview. Set as_markdown=true to convert HTML to Markdown for LLM context. |
| search_postsA | Read-only. Full-text search posts in a publication. Use for keyword discovery; for chronological browsing use list_posts. Returns titles + ids only (call get_post / get_post_content for details). |
| list_notesA | Read-only. List the authenticated user's own published Notes (short-form, Twitter-like). For a comment thread on a post use list_comments. For replies under one note, fetch via the note id. |
| list_commentsA | Read-only. Return the full nested comment tree for a post (parent + replies, with author handle, body, date, reaction count). To find only the threads YOU haven't replied to yet, use get_unanswered_comments. |
| get_feedA | Read-only. Pull items from the reader feed you'd see in the Substack app/home. Pass tab='for-you' (personalized recommendations, default), 'subscribed' (only publications you've subscribed to), or 'category-{slug}' for a topic feed (e.g. 'category-tech', 'category-finance', 'category-politics'). Returns a list of {post_id, title, pub, byline, snippet, published_at}. For a single publication's chronological list use list_posts; for keyword search use search_posts. |
| publish_noteA | WRITE. Publish a new top-level Note (short-form post). Defaults to dry_run=true (no network write); set dry_run=false to actually post. Idempotent via dedup hash on body. For a reply to an existing note use reply_to_note. For long-form posts, use Substack's editor (not exposed). |
| reply_to_noteA | WRITE. Reply to an existing Note (any author's). Defaults to dry_run=true. Dedup-protected: replays of the same body to the same note are no-ops. For replies to a post comment, use propose_reply -> confirm_reply (which run through the same safety stack). |
| comment_on_postA | WRITE. Add a NEW top-level comment under a post (not a reply to an existing comment). Defaults to dry_run=true. For replies to existing comments use propose_reply -> confirm_reply. Dedup-protected by (post_id, body) hash. |
| react_to_postA | WRITE. Add (on=true, default) or remove (on=false) a reaction on a post. Defaults to ❤ and dry_run=true. For comment-level reactions use react_to_comment. Reactions are not deduped (Substack itself idempotent). |
| react_to_commentB | WRITE. React on a comment (default ❤). Set kind='post' for comments under a post (uses the publication host) or kind='note' for replies on a Note (uses substack.com). Defaults to dry_run=true. |
| restack_postA | WRITE. Restack a post (Substack's reshare). Defaults to dry_run=true. Substack does NOT support unrestacking via the public API — once on, stays on. To restack a Note instead, use restack_note. |
| restack_noteA | WRITE. Restack a Note (Substack's reshare for short-form Notes), broadcasting it to your subscribers' feeds. Example: restack_note(note_id='123456789', dry_run=false). Defaults to dry_run=true so the first call is a no-op preview — set dry_run=false to actually publish. Like restack_post, Substack does not support un-restacking via the public API (on=false is a no-op). For long-form posts, use restack_post instead. |
| delete_commentA | DESTRUCTIVE WRITE. Delete one of YOUR own comments (or one on your publication if you're the owner). Cannot be undone. Set kind='post' to delete a post comment (uses pub host) or kind='note' for a note reply. Defaults to dry_run=true — you must explicitly set false. |
| bulk_draft_repliesA | WRITE TO LOCAL FILE (no Substack call). Generate reply drafts for every comment on a post (kind='post') or every reply on a note (kind='note') using the daemon-path LLM (host CLI: claude / cursor-agent / codex on PATH, or SUBSTACK_OPS_LLM_CMD). Output is a JSONL drafts file with action='proposed' per row; review, edit action to 'approved' or 'rejected', then send via send_approved_drafts. |
| send_approved_draftsA | WRITE. Sequentially post every entry in a drafts.json file where action=='approved'. Skips proposed/rejected/already-deduped rows. Honors rate_seconds throttle. Defaults dry_run=true; set false to actually post. Use force=true to bypass dedup (rare; reposts a previously-sent reply). |
| audit_searchA | Read-only. Query the local audit.jsonl log of every write this server has performed (or attempted). Filters compose with AND. Use to debug 'did I post that?' or to pull rate-limit history. For a quick count summary use dedup_status. |
| dedup_statusA | Read-only. Return counts from the local dedup SQLite DB (one row per successful write, keyed by content hash). Quick health check; for filtered details use audit_search. No args. |
| get_unanswered_commentsA | Read-only. Return comments on a post where the authenticated user has NOT yet replied (filters out the entire branch if you've replied anywhere in the ancestry). This is the canonical worklist tool: read each, draft a reply in your own context, then propose_reply -> confirm_reply per item. For the full unfiltered tree use list_comments. |
| propose_replyA | STAGE A WRITE (no Substack call yet). Validate a reply, compute its dedup hash, build the exact payload, store it under a token, return the token + preview. Show the preview to the user. On approval, call confirm_reply with the same token. Tokens expire in 5 minutes. kind='post' requires post_id + parent_comment_id (for replies under a comment); kind='note' requires note_id. For new top-level post comments use comment_on_post. |
| confirm_replyA | EXECUTE the staged write. Look up the token from propose_reply, post to Substack, log to audit.jsonl, persist dedup row. Idempotent: if the same content was already sent, returns {deduped: true} without re-posting. Use force=true to bypass dedup (rare). Tokens are single-use and expire 5 min after propose_reply. |
Prompts
Interactive templates invoked by user choice
| Name | Description |
|---|---|
No prompts | |
Resources
Contextual data attached and managed by the client
| Name | Description |
|---|---|
No resources | |
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