A local stdio Model Context Protocol (MCP) server and sync engine for the Substack posts and notes you have saved.
- 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_atorsaved_at). Search covers a post's full body text only after you fetch it once withget-contentor theget_post_contenttool. A normalsyncstores 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.
Install and run substack-saved-mcp with uv.
Install the published package from PyPI:
# Install system-wide into an isolated uv environment
uv tool install substack-saved-mcpTo 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.gitThe install puts substack-saved-mcp on your PATH:
# Verify installation
substack-saved-mcp --helpTo update or uninstall:
# Upgrade installed tool
uv tool upgrade substack-saved-mcp
# Uninstall tool
uv tool uninstall substack-saved-mcpTo 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# 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 serveAdd substack-saved-mcp to your MCP client's configuration file (e.g. claude_desktop_config.json).
{
"mcpServers": {
"substack-saved": {
"command": "substack-saved-mcp",
"args": ["serve"]
}
}
}To run from your checkout without installing:
{
"mcpServers": {
"substack-saved": {
"command": "uv",
"args": [
"--directory",
"/path/to/substack-saved-mcp",
"run",
"substack-saved-mcp",
"serve"
]
}
}
}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.sqliteif you setXDG_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"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 remainingSorting 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.
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.
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.
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.
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 instorage_state.json. - Note Sync & Write Tools (
sync_saved_notes,save_note,unsave_note,get_note_content):
Make direct API requests withstorage_state.json, with no browser page, headless or visible. - Interactive Login:
A browser window opens only when you runsubstack-saved-mcp loginin your terminal. If your session expires while you use an MCP client, the tool returns an error telling you to runsubstack-saved-mcp login.
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 installFor a development environment (uv sync):
uv run playwright installI 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--forcereplaces the installed version instead of skipping because one is present.--no-cachebuilds fresh instead of reusing a cached wheel from before your edits.