Substack Saved Posts & Notes MCP & CLI
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_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.
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.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"
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 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.
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
--forcereplaces the installed version instead of skipping because one is present.--no-cachebuilds 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)
| File | Size | Uploaded | |
|---|---|---|---|
| substack_saved_mcp-0.3.3.tar.gz | 182.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|