Skip to content

About

MCP server and sync engine for your saved/bookmarked Substack posts

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Substack Saved Posts & Notes MCP & CLI

PyPI version

A local stdio Model Context Protocol (MCP) server and sync engine for the Substack posts and notes you have saved.

Features

  • Read & Search: Full-text search (SQLite FTS5) over saved post titles, excerpts, authors, and publications. Filter by publication, audience tier (everyone, only_paid), reading progress, and date ranges (published_at or saved_at). Search covers a post's full body text only after you fetch it once with get-content or the get_post_content tool. A normal sync stores metadata and excerpts, so search matches posts you haven't opened on their title, excerpt, and metadata.
  • Reading Progress: Substack records how far you've read each saved post. Its mobile apps show this; the web UI doesn't. This tool stores it, so you can list the posts you haven't read or haven't finished, and sort by the minutes left. See Which saved posts haven't I read?.
  • Saved Notes: The tool syncs, searches, and caches Substack's short-form notes apart from posts. A note has an author and a body instead of a title or audience tier. Search covers note bodies, authors, and the titles of restacked posts. Notes skip the browser: sync, save, unsave, and content fetch are all plain authenticated API calls.
  • Full Content for LLMs: Fetch a saved post or note and get its content back as clean text, with headings, lists, and links kept readable. The tool caches the result for next time.
  • Save & Unsave: Bookmark posts and notes, or remove bookmarks. Posts use a headless browser session; notes use the API.
  • Offline Queries: Read and search tools query the local SQLite cache, with no network calls.
  • Local Credentials: Your Substack session stays on your machine, in a data directory only your user can read.
  • MCP Tools and Resources for both posts and notes over stdio.

Installation with uv

Install and run substack-saved-mcp with uv.

Option A: Install from PyPI as a system-wide tool (uv tool install)

Install the published package from PyPI:

# Install system-wide into an isolated uv environment
uv tool install substack-saved-mcp

To install from a local checkout instead:

# Navigate to the repository
cd /path/to/substack-saved-mcp

# Install system-wide into an isolated uv environment
uv tool install .

# Or install from the Git repository:
# uv tool install git+https://github.com/toniher/substack-saved-mcp.git

The install puts substack-saved-mcp on your PATH:

# Verify installation
substack-saved-mcp --help

To update or uninstall:

# Upgrade installed tool
uv tool upgrade substack-saved-mcp

# Uninstall tool
uv tool uninstall substack-saved-mcp

Option B: Development environment (uv sync)

To develop or modify the code:

# Clone and enter directory
cd substack-saved-mcp

# Install dependencies and dev tools (pytest)
uv sync --extra dev

# Run CLI commands using uv run
uv run substack-saved-mcp --help

# Run tests
uv run pytest

Quick Start

# 1. Initialize local database
substack-saved-mcp init

# 2. Authenticate with Substack (opens interactive browser window once)
substack-saved-mcp login

# 3. Sync saved posts AND notes into local cache (both entities by default)
substack-saved-mcp sync

# 3b. Or sync just one entity
substack-saved-mcp sync --only posts
substack-saved-mcp sync --only notes

# 4. Search saved posts via CLI
substack-saved-mcp search "artificial intelligence"

# 4b. Filter by publication or audience tier (see which tiers are cached with `audiences`)
substack-saved-mcp audiences
substack-saved-mcp list --audience only_paid
substack-saved-mcp search "artificial intelligence" --audience everyone

# 4c. Find posts you haven't read or haven't finished
substack-saved-mcp list --read-state unread
substack-saved-mcp list --read-state unfinished --sort-by minutes_remaining
substack-saved-mcp search "artificial intelligence" --read-state unread

# 5. Save or unsave a post
substack-saved-mcp save "https://example.substack.com/p/post-slug"
substack-saved-mcp unsave "https://example.substack.com/p/post-slug"

# 6. Get a saved post's full content, cleaned up and ready for an LLM
substack-saved-mcp get-content "https://example.substack.com/p/post-slug"

# 7. Work with saved notes the same way
substack-saved-mcp list-notes --limit 10
substack-saved-mcp search-notes "kubernetes" --author alice
substack-saved-mcp note-authors
substack-saved-mcp save-note "https://substack.com/@handle/note/c-123456"
substack-saved-mcp unsave-note "https://substack.com/@handle/note/c-123456"
substack-saved-mcp get-note "https://substack.com/@handle/note/c-123456"

# 8. Check combined status (counts, unread totals, last sync per entity)
substack-saved-mcp status

# 9. Launch stdio MCP server
substack-saved-mcp serve

Configuring MCP Clients (Claude Desktop, Goose, Cursor, etc.)

Add substack-saved-mcp to your MCP client's configuration file (e.g. claude_desktop_config.json).

Using the installed tool (uv tool or global binary)

{
  "mcpServers": {
    "substack-saved": {
      "command": "substack-saved-mcp",
      "args": ["serve"]
    }
  }
}

Using uv from the repository path

To run from your checkout without installing:

{
  "mcpServers": {
    "substack-saved": {
      "command": "uv",
      "args": [
        "--directory",
        "/path/to/substack-saved-mcp",
        "run",
        "substack-saved-mcp",
        "serve"
      ]
    }
  }
}

Frequently Asked Questions (FAQ)

Where is the database saved?

The SQLite database lives in your OS application data directory:

  • Linux / macOS: ~/.local/share/substack-saved-mcp/saved_posts.sqlite
    (or $XDG_DATA_HOME/substack-saved-mcp/saved_posts.sqlite if you set XDG_DATA_HOME)

Set a custom database path or directory with environment variables:

export SUBSTACK_SAVED_DB_PATH="/path/to/my/custom_database.sqlite"
# or
export SUBSTACK_SAVED_DATA_DIR="/path/to/my/data_dir"

Which saved posts haven't I read?

Filter by reading state on the CLI with --read-state, or pass read_state to the list_saved_posts and search_saved_posts MCP tools. In a chat, ask your assistant to "list my unread saved posts".

State Posts it matches
unread You never opened them, and Substack recorded no progress.
in_progress You read part, below the finished threshold.
finished You read past the threshold (default 95%).
unfinished Everything below the threshold: unread plus in_progress.
started You opened them or read any part.
substack-saved-mcp sync --force                      # refresh progress for every post first
substack-saved-mcp list --read-state unfinished --sort-by minutes_remaining --limit 20
substack-saved-mcp status                            # unread count and total minutes remaining

Sorting unfinished posts by minutes_remaining puts the quickest ones to finish at the top. Each post also reports is_fully_read and minutes_remaining. Notes have no reading progress.

How does the tool decide a post is fully read, and how fresh is that?

Substack reports a max_read_progress high-water mark (0.0 to 1.0) for each saved post. The tool counts a post as fully read once that value reaches a threshold, 0.95 by default, because finished posts on Substack tend to stop around 0.98 to 0.9999 rather than at 1.0. Change it with:

export SUBSTACK_SAVED_FULLY_READ_THRESHOLD="0.90"

A sync refreshes progress for each post it fetches. An incremental sync stops at the first posts it already has, so progress on older posts goes stale until you run substack-saved-mcp sync --force. A post you finished last week can show as unread until then.

Which Substack API does syncing saved posts use?

Sync tries Substack's unified reader API first. If that fails, it falls back to an older posts-only API, then to headless browser scraping. To force one source while troubleshooting, set:

export SUBSTACK_SAVED_POSTS_SOURCE="unified"  # or "legacy" or "dom"

Leave it unset, or set "auto", for the default fallback chain.

What does a "partial" sync status mean?

Substack sometimes rate-limits a sync (HTTP 429) so hard that a page of results fails even after retries. The sync then keeps what it fetched and reports status: partial instead of success, in substack-saved-mcp status and in the sync tool's response. On a sync --force, a partial run skips reconciliation, so the tool won't mark a post or note as unsaved because it couldn't fetch it. Run sync again later to pick up what it missed.

Will a browser window pop up when running as an MCP server?

No. During normal MCP use you won't see a browser window.

  • Read & Search Tools (search_saved_posts, list_saved_posts, get_saved_post, search_saved_notes, list_saved_notes, get_saved_note, list_publications, list_audiences, saved_posts_status):
    Query the local SQLite database offline. No browser.
  • Post Sync & Write Tools (sync_saved_posts, save_post, unsave_post, get_post_content):
    Run headless with the session stored in storage_state.json.
  • Note Sync & Write Tools (sync_saved_notes, save_note, unsave_note, get_note_content):
    Make direct API requests with storage_state.json, with no browser page, headless or visible.
  • Interactive Login:
    A browser window opens only when you run substack-saved-mcp login in your terminal. If your session expires while you use an MCP client, the tool returns an error telling you to run substack-saved-mcp login.

What if I get a Playwright "Executable doesn't exist" error?

An error like BrowserType.launch: Executable doesn't exist (often during login) means Playwright hasn't installed its browsers in the tool's isolated environment. Run playwright install inside that environment.

For a system-wide install (uv tool install):

~/.local/share/uv/tools/substack-saved-mcp/bin/playwright install

For a development environment (uv sync):

uv run playwright install

I edited the source code, but the installed substack-saved-mcp command still runs the old version. Why?

uv tool install copies the package into its own environment at install time and doesn't track your working tree. After you edit files under src/ or pull new commits, the installed copy keeps running the old code. uv run substack-saved-mcp ... from the repo uses your current source.

Reinstall from your working tree to pick up the changes:

uv tool install . --no-cache --force
  • --force replaces the installed version instead of skipping because one is present.
  • --no-cache builds fresh instead of reusing a cached wheel from before your edits.

About

MCP server and sync engine for your saved/bookmarked Substack posts

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages