Skip to main content
Glama

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
SUBSTACK_USER_IDNoYour Substack numeric user id
SUBSTACK_OPS_LLM_CMDNoOverride LLM command for daemon mode
SUBSTACK_SESSION_TOKENNoSubstack session cookie
SUBSTACK_PUBLICATION_URLNoYour Substack publication URL

Capabilities

Features and capabilities supported by this server

CapabilityDetails
tools
{
  "listChanged": false
}
prompts
{
  "listChanged": false
}
resources
{
  "subscribe": false,
  "listChanged": false
}
experimental
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
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

NameDescription

No prompts

Resources

Contextual data attached and managed by the client

NameDescription

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