Skip to main content

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.

Metadata

Release files for substack-saved-mcp 0.3.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for substack-saved-mcp 0.3.3
File Size Uploaded
substack_saved_mcp-0.3.3.tar.gz 182.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for substack-saved-mcp 0.3.3
File Interpreter ABI Platform
substack_saved_mcp-0.3.3-py3-none-any.whl Python 3 none any Details

Total release size: 235.5 kB

Release files / substack_saved_mcp-0.3.3.tar.gz

Download URL substack_saved_mcp-0.3.3.tar.gz
Size 182.3 kB
Tags Source
SHA-256 checksum
How to use checksums
0103e08876e9009ad52a06278b30a7522e2c660470dcda5b702b9c9aaab6b206
BLAKE2b-256 checksum
How to use checksums
3f2be404f59c03d98775b885f3a3967c961e7831e54048f1d4254cabeea64835
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / substack_saved_mcp-0.3.3-py3-none-any.whl

Download URL substack_saved_mcp-0.3.3-py3-none-any.whl
Size 53.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8e370c2cace2becb8c7a5f70639d0286ad8fdd6076fc6d91096d140f0444e33c
BLAKE2b-256 checksum
How to use checksums
daa8aa831483909a7bb68f23fca6a46c5c8ecd5c8982af9e5d830ce229e6ac19
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

This release

0.3.3 This release

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page