A side-by-side look at the Substack MCP and LinkedMash MCP servers: transport, auth, tools, quality signals, and the config to install either one in Claude, Cursor, Windsurf or VS Code.
Verdict: choose Substack MCP for social media work as a local process; choose LinkedMash for social media as a local process. You can also run both side by side in one mcpServers block.
SUBSTACK_PUBLICATION_URL, SUBSTACK_SESSION_TOKEN, SUBSTACK_USER_ID, SUBSTACK_MCP_HOME.get_subscriber_count, list_published_posts, list_drafts.search_docs, list_bookmarks, search_bookmarks.Rows where the two servers differ are marked.
| Spec | ||
|---|---|---|
| Category | Social Media | Social Media |
| Quality signal | 68/100 · Great | 56/100 · Good |
| Install command | Confirmed | Confirmed |
| Transport | Local subprocess (stdio) | Local subprocess (stdio) |
| Auth | other | Not declared |
| Pricing | Free | Not declared |
| Required env vars | SUBSTACK_PUBLICATION_URLSUBSTACK_SESSION_TOKENSUBSTACK_USER_IDSUBSTACK_MCP_HOME | None listed |
| Verified owner | Community listing | Community listing |
| GitHub stars | 44 | 0 |
| Directory activity | 6 views 0 installs 0 upvotes | 2 views 0 installs 0 upvotes |
| Tools | 14 | 40 |
| Full listing | View Substack MCP | View LinkedMash |
get_subscriber_countGet the current subscriber count for your Substack publication. Returns `precision`: 'exact' when the API reports a true count, 'approximate' when only Substack's rounded value is available (the real number is that or higher — render it hedged, e.g. '1,000+'), or 'unavailable' with count -1. Never treat an approximate value as exact.list_published_postsList published posts with pagination. Returns title, date, slug, and URL for each post.list_draftsList draft posts. Returns title, creation date, and audience for each draft.get_postGet the full content of a published post by ID. Returns title, body HTML, metadata.get_draftGet the full content of a draft post by ID. Returns title, body, metadata.get_post_commentsGet comments on a published post. Returns commenter name, comment body, date, and reaction counts.get_sectionsList your publication's sections (categories). Returns each section's id and name. Use a section id as `section_id` when creating or updating a draft to file it under that section.get_post_analyticsGet performance stats (views, emails sent/delivered/opened, signups, subscribes, estimated value, comments, reactions) for a published post by ID. Substack has no per-post stats endpoint, so this searches your 500 most recent published posts for the ID; returns a not-found note if it isn't among them.list_scheduled_postsList posts scheduled for future publication, soonest first. Read-only visibility into what's queued — scheduling itself is done in Substack's editor (this server does not schedule, publish, or delete long-form posts). Returns id, title, audience, and scheduled time (`trigger_at`).create_draftCreate a new draft post. Accepts markdown body which is converted to Substack's format. Does NOT publish — creates a draft only.update_draftUpdate an existing draft post. Only works on unpublished drafts. Accepts markdown body.upload_imageUpload an image to Substack's CDN. Provide exactly one of `image_base64` (a base64 data URI) or `image_path` (a local file path). Returns a hosted image URL that is publicly fetchable by anyone with the link (an unlisted asset — not attributed to you or added to your feed).search_docsAnswer a question about Linkedmash THE PRODUCT — its features and how to reach them, how to change a setting, and pricing/billing. Use this for questions like 'where do I manage my subscription', 'how do I schedule a post', 'how much is the Creator plan', 'how do I change Lina's writing rules', 'how do I import my LinkedIn saves', 'what does Smart Folders do'. It returns the most relevant sections of the Linkedmash help guide — answer the user in your own words from them and point them to the exact page (e.g. Settings → Billing). For live prices, direct the user to the pricing page (/pricing). This tool reads product documentation only, NOT the user's saved posts or account data.list_bookmarksList bookmarks with optional filtering and pagination. Defaults: read AND unread bookmarks are both included (set is_unread_only=true only if the user asks for unread); archived bookmarks are excluded (set hide_archived=false to include them). Only apply the filters the user actually asked for — do NOT add media_type, unread, or date filters unprompted. IMPORTANT: Use this tool (with the 'author' parameter) when the user asks to show posts by a specific author, find posts from someone, or filter by author name. The 'author' parameter supports fuzzy matching of author names.search_bookmarksSearch through a user's LinkedIn bookmarks using either keyword (text) search or semantic (meaning-based) search. Supports all list_bookmarks filters, full-text search via the q field, and semantic search via the vector_search_term field for natural language or topic-based queries. IMPORTANT: At least one of 'q', 'vector_search_term', or 'author' must be provided. If the user asks to find posts by a specific author (e.g., 'Show posts by John Doe', 'Find posts from Suresh sambantham'), use the 'author' parameter instead of putting the author name in the 'q' field. Use 'q' for searching post content, not for filtering by author.get_bookmark_countGet the count of bookmarks matching the given filters. Supports the SAME filters as list_bookmarks (dates, author, tag, media_type, unread, q, comment) and the count respects all of them, so it is always consistent with a list_bookmarks call using identical filters. Defaults match list_bookmarks: read + unread included, archived excluded. The result echoes applied_filters — check it matches what the user asked before reporting the number.list_labelsList all user's labels/tagsadd_labelsAdd a label to a FEW explicitly chosen posts (specific post_ids the user picked or is looking at). Either label_id or label_name must be provided. NEVER use this to label 'all posts matching a filter' (a date range, an author, a tag, unread…) — fetched lists are ONE PAGE, so labeling their ids silently misses the rest; use tag_bookmarks_by_filter instead, which labels EVERY match server-side in one call and returns matched/tagged counts.remove_labelsRemove labels from bookmarked posts. Either label_id or label_name must be provided.tag_bookmarks_by_filterAdd a label to ALL bookmarks matching a filter in ONE server-side call. Use this — NOT list_bookmarks/search_bookmarks followed by add_labels — whenever the user asks to tag/label many posts by date range, author, tag, media type, or unread status (e.g. 'tag all posts older than March 2026 as Archive', 'label everything from John Doe as Hiring'). Date targeting: 'older than'/'before' DATE -> posted_to (or bookmarked_to); 'since'/'after' DATE -> posted_from (or bookmarked_from); use posted_* for when the post was published and bookmarked_* for when the user saved it; dates are ISO (YYYY-MM-DD). Provide label_name (created if it doesn't exist) or label_id. The tool returns matched/tagged/failed counts: ALWAYS tell the user how many posts were tagged; if 'truncated' is true, tell them it stopped at the cap and to narrow the date range to tag the rest; if 'failed' > 0, mention it. Never paginate or loop add_labels for bulk tagging — this one call handles the whole set.remove_labels_by_filterREMOVE a label from ALL bookmarks matching a filter in ONE server-side call — the mirror of tag_bookmarks_by_filter. Use this — NOT list/search + remove_labels on page ids — whenever the user asks to remove/clear a label from many posts by date range, author, tag, media type, or unread status (even phrased as 'fetch all X and remove the label'). Provide label_name or label_id; matching is automatically scoped to posts that carry the label. Returns matched/removed/failed counts: ALWAYS report them ('Removed from X of Y matching posts'); if 'truncated' is true, say it stopped at the cap.archive_bookmarks_by_filterARCHIVE all bookmarks matching a filter in ONE server-side call (reversible — archived posts stay recoverable in the app's Archive). Use for 'archive all posts <filter>' style requests, even phrased as 'fetch then archive'. BEFORE calling: tell the user what will be archived (use get_bookmark_count with the same filter) and get an explicit yes — then call with confirm=true. Returns matched/affected counts: ALWAYS report them; if 'truncated' is true, say it stopped at the cap.delete_bookmarks_by_filterDELETE all bookmarks matching a filter in ONE server-side call. Deletion is soft (recoverable from the app for a while) but MUST be treated as destructive: BEFORE calling, tell the user exactly what will be deleted (use get_bookmark_count with the same filter to state the number) and get an explicit yes in this conversation — only then call with confirm=true. NEVER delete on your own initiative. Returns matched/affected counts: ALWAYS report them; if 'truncated' is true, say it stopped at the cap and offer to run again with a narrower filter.setup_vector_storeInitialize the vector store for semantic search. CRITICAL: When vector/semantic search fails with 'Vector Store Not Setup' error and you offered the user options to either initialize or use keyword search, and the user responds with 'first', 'option 1', 'yes', 'initialize', 'set it up', 'init', 'setup', 'go ahead' or similar confirmation, you MUST immediately call this tool. ALWAYS check your previous message in the chat history to confirm the user is responding to your options. Do NOT re-run the search tool - just call this tool. After calling this tool, inform the user that setup has started and may take a few minutes. This is an async operation that may take a few minutes to complete for large bookmark collections.Paste either block (or both) into your client config, e.g. claude_desktop_config.json or ~/.cursor/mcp.json. We only show a config when the install command is confirmed.
{
"mcpServers": {
"conorbronsdon-substack-mcp": {
"command": "npx",
"args": [
"-y",
"@conorbronsdon/substack-mcp"
],
"env": {
"SUBSTACK_PUBLICATION_URL": "YOUR_SUBSTACK_PUBLICATION_URL_HERE",
"SUBSTACK_SESSION_TOKEN": "YOUR_SUBSTACK_SESSION_TOKEN_HERE",
"SUBSTACK_USER_ID": "YOUR_SUBSTACK_USER_ID_HERE",
"SUBSTACK_MCP_HOME": "YOUR_SUBSTACK_MCP_HOME_HERE"
}
}
}
}
{
"mcpServers": {
"linkedmash-2": {
"command": "npx",
"args": [
"-y",
"@linkedmash/mcp"
]
}
}
}