Releases: toniher/substack-saved-mcp
Release list
0.3.2
Highlights
- SSRF hardening on authenticated browser navigation.
save_post,unsave_post, andfetch_post_content(reachable from both the CLI and the MCP tools exposed to LLM clients) now reject any non-HTTPS URL via a new_require_https()guard before Playwright's authenticated browser context navigates to it. This closes a path where a caller-supplied URL — including one reachable via prompt injection into an LLM client — could reachfile://paths or internal/cloud-metadata HTTP endpoints (e.g.169.254.169.254). - Session credential file hardening. The
storage_state.jsonsession file written duringloginis now pre-created with owner-only0o600permissions before Playwright populates it, closing a brief window where it could be created world/group-readable under a permissive umask.chmodfailures on the credentials file and its parent directories are now logged instead of silently swallowed. - Dedup cleanup. Extracted
_with_playwright()(shared Playwright instance dispatch),_resolve_row()(posts/notes URL-or-ID lookup), and_last_sync_info()(per-entity sync-status query) helpers, plus module-levelSTEALTH_LAUNCH_ARGS/STEALTH_CONTEXT_KWARGSconstants, removing repeated logic acrosssubstack_client.py,database.py, andcli.py. - Dropped the unused
pytest-asynciodev dependency.
Full changelog
0.3.1
Highlights
-
Reading-progress tracking &
read_statefiltering. Substack exposes per-post reading progress (read_progress/max_read_progress/is_viewed) on both unified and legacy reader APIs. They are now stored as columns and surfaced viaread_statefilters (unread/in_progress/finished/started) onlist_posts/search_postsand MCP tools, with sorting byread_progressandminutes_remaining(word_count * (1 - max_read_progress)).is_fully_readandminutes_remainingare derived at read time from the high-water markmax_read_progressagainst a configurableSUBSTACK_SAVED_FULLY_READ_THRESHOLD(default0.95, envSUBSTACK_SAVED_FULLY_READ_THRESHOLD).get_statusnow reportsposts_unread/posts_in_progress/posts_fully_read/minutes_remaining_total.upsert_postnow preserves0.0progress values (previously lost toor-coalescing). -
inspect-networkstability fixes. Removes thecontext.route("**/*")→route.fetch()→route.fulfill()interception that caused cross-tab stalls, CORS breakage, andcontent-encoding: gzipdouble-decompression (React feed failed to render). Replaced with a passivecontext.on("response")listener so every page asset and API call loads natively. Scopes the browser launch with--disable-blink-features=AutomationControlled, a desktop Chrome user-agent and1280×800viewport to pass Cloudflare Turnstile / cookie-banner checks. -
Rate-limit resilience. Raises reader-API
max_retriesfrom 3→5 (withRetry-After-aware capped exponential backoff, 2–30 s) across all three cursor-paginated fetchers (legacy posts, unified posts, notes). Adds a 15 s cooldown between posts and notes phases ofsyncwhen the posts phase was truncated by rate-limiting, so the notes phase no longer immediately re-trips the same bucket. -
Sync warning hygiene. Explicit
api_context.dispose()infinallyblocks for all Playwright request contexts (_fetch_saved_posts_page_impl,_fetch_saved_notes_page_impl,probe_api) suppressesTask was destroyed but it is pending!/TargetClosedErrornoise on sync completion after a mid-pagination 429.
Full changelog
0.3.0
Highlights
- Saved posts now use Substack's unified reader API by default. A live parity comparison found it a strict superset of the older posts-only reader API (985 vs 1079 posts on a real account, with the legacy API's count also unstable run-to-run), with
saved_at/ordering/wordcountall confirmed at 100%. The legacy API and DOM scraping remain automatic fallbacks;SUBSTACK_SAVED_POSTS_SOURCE(auto/unified/legacy/dom) can force a specific source. - Fixed a silent-truncation bug shared by all three cursor-paginated fetchers (posts, unified posts, notes): a persistent 429 mid-pagination used to return a partial list indistinguishable from a complete one. A
--forcesync now detects this and skips reconciliation for that run instead of risking soft-deleting posts/notes that were merely unreachable, not actually unsaved. Reported via a newpartialsync status. - New diagnostic commands:
probe-api(headless GET of any known API URL) andcompare-saved-apis(parity report between the legacy and unified endpoints) for verifying Substack API behavior without guesswork. - Fixed
inspect-network's response-body capture, which was silently recording zero bodies due to a sync-API deadlock risk; now uses request interception. - Fixed a version-string drift:
--versionand the package's__version__had been stuck at0.1.0since a previous release.
Full changelog
Support Saved Notes
Adds full support for Substack's saved notes alongside posts — sync, search, list, save/unsave, and full-content fetch, backed by a dedicated notes table and FTS5 index. All notes operations use direct authenticated API calls, no browser required. New CLI commands (list-notes, search-notes, note-authors, save-note, unsave-note, get-note) plus matching MCP tools and resource. Also fixes a few latent bugs in the posts code: a crash on URL-changing upserts, filters silently dropped by search's fallback path, and inaccurate sync timestamps.
Initial release
Initial release of Substack saved favourites manager CLI and MCP