substack-saved-mcp
A local MCP server and CLI for syncing, searching, and reading your saved Substack posts and notes offline from a SQLite cache, with optional remote save/unsave actions.
Search saved posts full-text (title, excerpt, author, publication, fetches content) with filters for publication, audience tier, read state, and published/saved date ranges.
List saved posts with pagination, sorting (
saved_at,published_at,read_progress,minutes_remaining), and publication/audience/read-state filters.Retrieve full post details (metadata, timestamps, cached content) by URL or local ID.
Save/unsave posts remotely on Substack (best-effort confirmation) and update the local cache.
Fetch cleaned post content as readable plain text for LLM consumption, cached for reuse.
List publications and audience tiers with post counts from the cache.
Get cache status: totals for saved/unsaved posts, publications, unread/in-progress/fully-read counts, minutes remaining, and last sync info.
Sync saved posts incrementally or with a forced full resync.
Search and list saved notes full-text with author/restack filters and posting/saved date ranges.
Retrieve full note details (author, body, engagement counts, restack info).
Save/unsave notes via Substack's API (no browser) and soft-delete locally.
Fetch cleaned note content for LLM use, cached.
Sync saved notes incrementally or fully.
Manages saved/bookmarked Substack posts, including syncing, searching, retrieving full content, and toggling save/unsave status.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@substack-saved-mcpshow me my saved posts about AI"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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.
Related MCP server: substack-article-mcp
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-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-mcpOption 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 pytestQuick 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 serveConfiguring 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 |
| You never opened them, and Substack recorded no progress. |
| You read part, below the finished threshold. |
| You read past the threshold (default 95%). |
| Everything below the threshold: |
| 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.
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 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.
Available Tools
17 toolsget_note_contentA
Fetch a saved note's full content, cleaned and formatted for LLM consumption.
Returns the cached body_text if a previous fetch already stored it, unless force_refetch is set. Otherwise fetches the note directly via Substack's reader API (no browser page needed) and caches the result. Requires an active authenticated Substack session.
| Name | Required | Description | Default |
|---|---|---|---|
| url_or_id | Yes | ||
| force_refetch | No |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the full burden. It transparently discloses caching behavior, the use of an API instead of a browser, and the requirement for an authenticated session. This covers key behavioral aspects, though it does not mention error handling or response details beyond the output schema.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is four concise sentences, front-loaded with the primary purpose, and each sentence adds relevant detail without redundancy. It avoids jargon and is well-structured for quick consumption by an AI agent.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a tool with only two parameters and an output schema, the description covers essential context: caching, API access, authentication, and the cleaning/formating. It is complete enough for most use cases, though it omits potential edge cases like failure modes or idempotency guarantees, which are not critical here.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0%, requiring the description to compensate. It explains force_refetch's effect on caching but does not clarify what url_or_id actually accepts beyond the schema's generic string type. This partial compensation leaves a gap for the primary parameter.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the verb 'Fetch' and the resource 'a saved note's full content', with additional value of 'cleaned and formatted for LLM consumption'. This distinguishes it from sibling tools like get_post_content or get_saved_note by explicitly focusing on notes and formatting for LLMs.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
While it mentions the caching mechanism and the API-based fetch, it does not explicitly contrast with alternatives or state when to prefer this over get_saved_note or get_post_content. It implies usage for fetching note content but lacks explicit when-not-to-use guidance or alternative recommendations.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_post_contentA
Fetch a saved post's full content, cleaned and formatted for LLM consumption.
Returns the cached content_text if a previous fetch already stored it, unless force_refetch is set. Otherwise fetches the post's page directly, extracts its body_html from Substack's server-rendered window._preloads blob, converts it to plain text (headings, list items, and links kept readable), and caches the result. Requires an active authenticated Substack session. If the content can't be located on the page (e.g. Substack changed how it embeds it, or the post is paywalled beyond this account's access), returns success=False with a message suggesting the caller run 'substack-saved-mcp inspect-network' while opening the post so the real content source can be captured.
| Name | Required | Description | Default |
|---|---|---|---|
| url_or_id | Yes | ||
| force_refetch | No |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description fully carries the transparency burden. It discloses caching behavior, the fetch source (window._preloads blob), text conversion process, auth requirements, and failure mode with a suggested remediation. This is exceptional behavioral disclosure for a read tool, covering side effects and edge cases.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is compact yet detailed, with three sentences covering purpose, behavior, and failure handling. The second sentence is long and clause-heavy, but every piece of information earns its place given the tool's complexity. It is front-loaded with the core purpose and avoids unnecessary repetition.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's complexity, no annotations, and an output schema present, the description is remarkably complete. It covers caching, extraction method, auth prerequisites, and failure handling with a user- actionable next step. Nothing critical is omitted for an agent to invoke this tool safely and effectively.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema has zero descriptions, but the tool description explicitly explains force_refetch semantics (overrides cached content) and implies url_or_id identifies the saved post. While the exact format for url_or_id isn't specified, the parameter name and context are sufficiently self-explanatory for an agent. The description compensates well for the schema's lack of detail.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool fetches a saved post's full content, cleaned and formatted for LLM consumption. This specific verb+resource combination distinguishes it from siblings like list_saved_posts or get_saved_post, which likely handle metadata or listings.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description establishes clear context: use when you need the full content of a saved post, with prerequisites (authenticated session) and caching behavior. It does not explicitly name alternative tools for different use cases, but the purpose is unambiguous and the troubleshooting tip for failed fetches adds practical guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_saved_noteA
Retrieve full cached note details (author, body, engagement counts) by URL or local ID.
| Name | Required | Description | Default |
|---|---|---|---|
| url_or_id | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The description clearly identifies that this is a read operation returning cached data, but with no annotations present, more context is needed—such as staleness implications, error handling, or authentication requirements. It is not misleading but stays surface-level.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single, front-loaded sentence that communicates action, resource, and details without redundancy. Every clause adds value and no space is wasted.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple read tool with an output schema, the description sufficiently covers purpose and parameter semantics. It does not address edge cases like missing or invalid IDs, but overall it gives an adequate mental model for an agent to select and use the tool.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The parameter `url_or_id` is given meaningful explanation: it can be a URL or a local identifier. Since the schema description coverage is 0%, this is essential additional context. A concrete example or format hint would have been useful but is not required for the one-parameter tool.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb ('Retrieve'), specific resource ('full cached note details'), and enumerates returned fields ('author, body, engagement counts'). This clarifies the tool's scope and distinguishes it from sibling tools like get_note_content or search_saved_notes.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides no guidance about when to choose this tool over alternatives such as get_note_content or sync_saved_notes. It specifies the lookup mechanism (by URL or local ID) but does not explain contexts, exclusions, or constraints.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_saved_postA
Retrieve full cached post details, timestamps (published_at and saved_at), and content by URL, local ID, or Substack post ID.
content_text is empty until get_post_content has fetched the post.
| Name | Required | Description | Default |
|---|---|---|---|
| url_or_id | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full behavioral burden, and it does disclose one genuinely valuable trait: content_text is empty until get_post_content has fetched the post, revealing a cache-state dependency. It says nothing about authentication, whether the call hits the network or only a local cache, or what happens when the URL/ID is not saved — significant gaps for a tool with zero annotation coverage.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences: the first front-loads the capability and its accepted inputs, the second carries the one caveat an agent needs. No filler, no repetition of the tool name, and nothing that could be trimmed without losing information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
A return value schema exists, so the description need not re-explain output structure — yet it still names the key fields, which helps. Input documentation is thin in the schema and partially rescued here, but with no annotations the definition should say more about failure modes (unsaved post) and whether lookup is cache-only.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0% — url_or_id is documented only as a bare string. The description compensates by telling the agent the three accepted identifier forms (URL, local ID, Substack post ID), which is real added meaning beyond the schema. It still does not clarify which form to prefer or how ambiguous inputs are resolved.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb (Retrieve) and resource (full cached post details) and enumerates what comes back: timestamps published_at/saved_at and content. The reference to get_post_content hints at the boundary with that sibling, but it never frames the distinction as 'use this instead of X', so differentiation is implied rather than stated.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
There is no explicit when-to-use or when-not-to-use clause, and no comparison against siblings like get_post_content, get_saved_note, or list_saved_posts. The final sentence implies a precondition (content is only populated after a prior get_post_content call), which is useful but leaves the agent to infer the rest.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_audiencesA
List distinct audience tiers present in local cache with post counts.
Discovers actual values in use (e.g. "everyone", "only_paid") rather than a hardcoded enum, since Substack's audience values aren't officially documented and may vary or grow over time.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the burden of behavioral disclosure. It explains that values are discovered from local cache and are dynamic, but it does not explicitly state safety traits (e.g., read-only, no side effects) or mention whether the cache might be stale or require syncing beforehand.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is concise, with the core function stated first and the rationale in the second sentence. Every sentence provides value and there is no redundancy or unnecessary detail.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the simplicity (0 params) and presence of an output schema, the description is mostly complete. It covers what the tool does, where it reads from, and why it uses live values. It could mention prerequisites like cache syncing, but that is a minor gap.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool has zero parameters and the schema coverage is 100%, so the baseline is 4. The description adds context about why there are no parameters (it scans actual values in use) which is helpful, though not strictly necessary.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states it lists distinct audience tiers from local cache with post counts. It differentiates itself from sibling tools by focusing on audience tiers specifically, and the context about discovering actual values rather than hardcoded enums adds clarity.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implies usage for discovering dynamic audience values rather than relying on a static enum, but it does not explicitly state when to use this tool versus alternatives or mention any exclusions. No sibling tool is referenced for comparison.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_publicationsA
List all publications in local cache with post counts.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations are absent, so the description must disclose behavioral traits. The description only mentions listing local cache with post counts, but does not state whether the operation is read-only, requires prerequisites, or has side effects. It also does not clarify what 'local cache' means operationally.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single, well-structured sentence that front-loads the core purpose and includes relevant detail ('with post counts'). No redundancy or unnecessary text.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Though the tool is simple with no parameters and an output schema, the description lacks critical contextual details such as whether it is a read-only operation, how 'local cache' is populated, and when to choose this over sibling tools. Given the importance of an agent knowing side effects and usage, the description is minimally viable but has clear gaps.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool has zero parameters, so the input schema is inherently fully covered. The description correctly omits parameter details, warranting the baseline score of 4 for no-parameter tools.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool lists all publications in local cache and includes post counts, specifying the exact resource and scope. It is distinct from sibling tools like list_saved_posts, which focuses on saved posts rather than publications.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
No guidance is provided regarding when to use this tool instead of alternatives like search_saved_posts or sync_saved_posts. There are no explicit context cues, prerequisites, or exclusions.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_saved_notesA
List cached saved notes with pagination and optional author/restack filters.
sort_by can be 'posted_at' (when the note was posted) or 'saved_at' (always None for notes, so it gives no meaningful order). restacks_only limits results to notes that restack a post.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| author | No | ||
| offset | No | ||
| sort_by | No | saved_at | |
| restacks_only | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden, and it does disclose a genuinely non-obvious quirk: the default sort_by ('saved_at') yields no meaningful ordering for notes, and 'cached' signals results may be stale relative to the server. It omits any statement on auth requirements, cache staleness/sync behavior, or rate limits.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three short sentences, front-loaded with purpose before parameter nuance, with no filler. The saved_at parenthetical is slightly convoluted and could be tightened, but nothing is wasted.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need not be explained, and the description covers purpose, filters, and sorting. It still leaves gaps on the cached/sync relationship, how it differs from search_saved_notes, and pagination defaults for a 5-parameter tool.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0% across 5 parameters, so the description must compensate and only partially does. It explains sort_by's two values and what restacks_only does, but limit, offset, and author are covered at best vaguely by 'pagination and optional author/restack filters' with no accepted format or author identifier semantics.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (List) and resource (cached saved notes) plus the supported filters, which separates it from the sibling search_saved_notes. It stops short of naming that sibling explicitly, so an agent still has to infer list-vs-search, but the core purpose is unambiguous.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is only implied: 'cached' hints that sync_saved_notes may be needed first and the list/search split is left to inference. The note that sort_by='saved_at' produces no meaningful order is a useful selection hint (prefer 'posted_at'), but no explicit when/when-not or alternative tool is named.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_saved_postsA
List cached saved posts with pagination and optional publication/audience filters.
sort_by can be 'saved_at', 'published_at', 'read_progress', or 'minutes_remaining'. audience filters by tier (see list_audiences for cached values, e.g. "everyone", "only_paid"). read_state filters by reading progress: 'unread', 'in_progress', 'finished', 'unfinished' (unread + in_progress), or 'started' — a post is 'finished' once its high-water reading progress crosses a threshold, default 0.95.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| offset | No | ||
| sort_by | No | saved_at | |
| audience | No | ||
| read_state | No | ||
| publication | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full behavioral burden. It discloses non-obvious semantics (the 'unfinished'/'started' groupings, the high-water threshold defaulting to 0.95, and that results are cached), but says nothing about authorization needs, staleness of the cache, or whether the read is safe/non-mutating.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the core action, then grouped parameter guidance with no filler sentences. The block is somewhat long but every line conveys distinct filter semantics.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need not be described. For a 6-parameter, 0%-schema-coverage tool the description covers the semantically hard filters well, though the three remaining parameters (limit, offset, publication) are only implied.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate and it largely does: it enumerates sort_by values, explains the audience tier concept with a sibling reference, and defines each read_state value precisely. It leaves limit, offset (pagination is only mentioned), and publication unexplained, which keeps it short of a 5.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource ('List cached saved posts') plus the scope of its filters. It doesn't explicitly distinguish itself from the nearby search_saved_posts, though 'cached' and the filter set gesture at the difference.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It routes the agent to list_audiences for valid audience values, which is genuine usage guidance. However, there is no statement of when to use this versus search_saved_posts or saved_posts_status, so the primary selection decision is left to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
saved_posts_statusA
Return cache statistics, database path, and last sync run status.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| posts_unread | No | |
| database_path | Yes | |
| last_sync_status | No | |
| posts_fully_read | No | |
| posts_in_progress | No | |
| total_saved_notes | No | |
| total_saved_posts | Yes | |
| total_publications | Yes | |
| total_unsaved_notes | No | |
| total_unsaved_posts | Yes | |
| last_successful_sync | No | |
| last_note_sync_status | No | |
| minutes_remaining_total | No | |
| last_successful_note_sync | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden. It discloses the tool is read-only in nature (returns statistics and status) but doesn't state whether it triggers any side effects or has any rate limits. It doesn't describe what 'last sync run status' entails (e.g., success/failure details, timestamps) or what the database path refers to. The description is adequate but not rich in behavioral disclosure.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single, efficient sentence that states the exact content of the return. It is appropriately sized for a zero-parameter tool that retrieves status information, with no wasted words.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool is simple with no parameters and the output schema exists (though not detailed here), the description covers what the tool does and what it returns. It doesn't explain the format of the output statistics or status, but the output schema presumably provides that. For a status-check tool of this simplicity, the description is mostly complete, though it could benefit from clarifying whether this is a read-only operation.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool has zero parameters, so the input schema is empty (schema coverage 100% by default). The description correctly doesn't mention parameters since there are none. Per the rubric, 0 params gives a baseline 4, and the description appropriately focuses on what the tool returns rather than inputs. It adds value by specifying the exact output categories.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool returns three specific pieces of information: cache statistics, database path, and last sync run status. This distinguishes it from the sibling tools that operate on posts or notes directly, since this tool is about system status rather than content.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implies a diagnostic or monitoring use case ('Return cache statistics, database path, and last sync run status'), but does not explicitly state when to use this tool instead of other sync-related tools like sync_saved_posts or sync_saved_notes. There are no explicit exclusions or alternatives mentioned, leaving the agent to infer that this is for checking sync status without triggering a sync.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
save_noteA
Bookmark a Substack note remotely on Substack and save it to the local cache.
Requires an active authenticated Substack session (run 'substack-saved-mcp login' if expired). remote_confirmed=False means the note is still cached locally, but the tool could not verify the bookmark was actually created on Substack's side — a subsequent 'sync --force' will correct the local cache if the remote save didn't actually happen.
| Name | Required | Description | Default |
|---|---|---|---|
| url | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries full responsibility. It discloses the remote bookmark action, local caching, the authentication prerequisite, the remote_confirmed=False failure state, and the corrective sync behavior—this is substantial and actionable behavioral context.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is concise, front-loaded with the purpose, and efficiently uses three sentences to cover action, prerequisite, and failure behavior. No filler or redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a single-parameter tool with an output schema, the description fully covers the essential context: authentication requirement, what the tool does, what failure looks like, and how to recover. Return-value details are unnecessary because an output schema exists.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema has 0% description coverage and only lists 'url' as a required string. The tool description implies 'url' is a Substack note URL, which adds limited meaning, but it doesn't explain URL format, validation, or any constraints beyond the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a specific verb ('Bookmark') and resource ('Substack note'), and clarifies the dual action of remote bookmarking and local caching. This clearly distinguishes it from siblings like save_post, unsave_note, and sync_saved_notes.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides clear usage context: it requires an active authenticated Substack session, mentions what to do if expired, and explains the recovery path when remote confirmation fails. It does not explicitly name alternative tools or say when not to use it, but the intended use is well implied.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
save_postA
Bookmark a Substack post remotely on Substack and save it to the local cache.
Requires an active authenticated Substack session (run 'substack-saved-mcp login' if expired). Confirmation comes from Substack's save API (keyed by the post's numeric ID, read from the page) or, failing that, from a best-effort bookmark-button click whose rendered state is checked. remote_confirmed=False means the post is still cached locally, but the tool could not verify the bookmark was actually created on Substack's side — a subsequent 'sync --force' will correct the local cache if the remote save didn't actually happen.
| Name | Required | Description | Default |
|---|---|---|---|
| url | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the full burden, and it delivers: it discloses the remote-then-local write pattern, that confirmation comes from Substack's save API or a best-effort button click, and the meaning of remote_confirmed=False (locally cached but unverified). It also names the remediation ('sync --force'), which is exactly the kind of behavioral context an agent needs for a mutation tool.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loads the core action, then the auth prerequisite, then the confirmation semantics in a logical order. It is slightly long and the confirmation-mechanism detail is dense, but every sentence conveys actionable behavior rather than filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need no explanation. Combined with the auth requirement, local-vs-remote semantics, and failure/remediation behavior, the description covers everything an agent needs to invoke this correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0% and there is one required 'url' parameter with no type/format guidance in the schema. The description partially compensates by noting the post's numeric ID is read from the page, implying the input is a post page URL, but it never specifies whether a full URL, slug, or ID is expected.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (bookmark/save) and resource (Substack post) plus the dual scope: remote on Substack and local cache. An agent can immediately distinguish it from unsave_post and save_note, which operate on different actions or resources.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives a clear prerequisite and recovery path: requires an authenticated Substack session and tells the agent to run 'substack-saved-mcp login' if expired. It implies the save-vs-unsave dichotomy via naming but never states an explicit when-not or names an alternative tool.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_saved_notesA
Perform full-text FTS5 search across cached saved notes.
Searches body text, author name/handle, and (for restacks) the attached post's title. query uses SQLite FTS5 syntax. Allows filtering by author (name or handle) and original note date (posted_after/posted_before, ISO 8601 strings). Substack exposes no bookmark time for notes, so saved_at is always None and saved_after/saved_before match nothing.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| query | Yes | ||
| author | No | ||
| saved_after | No | ||
| posted_after | No | ||
| saved_before | No | ||
| posted_before | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full behavioral burden and does add real context: it reveals the FTS5 query syntax requirement, the exact searchable fields, and the important limitation that saved_at is always None so saved_after/saved_before are useless. It does not state read-only status or the implicit prerequisite that notes must be synced ('cached') before searching.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loads the core purpose in the first sentence, then layers in search scope, filter semantics, and the saved_at caveat. Every sentence adds distinct information with no redundancy or filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need not be described, and the description covers most parameters plus the FTS5 syntax requirement and the saved_at limitation. The gaps are minor: no limit/pagination semantics and no explicit read-only assurance or sync prerequisite.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate and largely does: it defines query as FTS5 syntax, author as name-or-handle, posted_after/posted_before as ISO 8601 strings, and explicitly explains that saved_after/saved_before match nothing. Only limit is left undocumented, a minor omission given six of seven parameters are clarified.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('full-text FTS5 search across cached saved notes') and further specifies exactly which fields are searched (body text, author name/handle, restack post title). The resource 'saved notes' cleanly distinguishes it from the sibling search_saved_posts, so an agent can select it without opening the schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explains capabilities and filter semantics but never says when to prefer this over list_saved_notes or search_saved_posts, nor names any alternative. It does implicitly warn against the saved_after/saved_before filters by noting they match nothing, which is useful but parameter-level rather than tool-selection guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_saved_postsA
Perform full-text FTS5 search across cached saved posts.
Searches title, excerpt, publication name, author, and content text (content is only present for posts fetched via get_post_content). query uses SQLite FTS5 syntax; a malformed query falls back to a substring match. Date filters take ISO 8601 strings (e.g. "2026-01-31"). Allows filtering by publication name, audience tier (see list_audiences for cached values, e.g. "everyone", "only_paid"), original post date (published_at), saved date (saved_at), and read_state ('unread', 'in_progress', 'finished', 'unfinished' (unread + in_progress), or 'started' — a post is 'finished' once its high-water reading progress crosses a threshold, default 0.95).
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| query | Yes | ||
| audience | No | ||
| read_state | No | ||
| publication | No | ||
| saved_after | No | ||
| saved_before | No | ||
| published_after | No | ||
| published_before | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations exist, so the description carries the burden, and it delivers: malformed FTS5 queries fall back to substring matching, date inputs are ISO 8601, and 'finished' status is defined by a reading-progress threshold defaulting to 0.95. It omits result ordering, pagination behavior, and confirmation of read-only/side-effect profile.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The core operation is front-loaded and most sentences carry substantive information. It is somewhat long and the read_state parenthetical is awkwardly worded, but there is little outright filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With 9 parameters, no annotations, and an output schema that handles return values, the description supplies the semantic detail an agent needs for nearly every filter plus a key behavioral caveat. Missing ordering and pagination guidance keeps it from fully complete.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate and largely does: it explains query syntax plus fallback, enumerates read_state values with definitions, distinguishes published_at from saved_at, gives audience examples, and specifies ISO 8601 for all date filters. Only 'limit' is left without descriptive meaning, but its default is visible in the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Perform full-text FTS5 search across cached saved posts') and names the searched fields (title, excerpt, publication, author, content). This is distinguishable from search_saved_notes and list_saved_posts by resource and mechanism alone.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives real usage context: it routes the agent to list_audiences for valid audience values and warns that content text is only present for posts fetched via get_post_content, implying a prerequisite. It stops short of stating when to prefer list_saved_posts over this search or when not to use it.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
sync_saved_notesA
Trigger incremental or full resync of saved notes from Substack account into local SQLite cache.
Incremental (default) stops early once it reaches notes already saved locally. force=True fetches the whole list and soft-deletes local notes no longer saved on Substack. Returns a SyncRun whose status is 'success', 'partial' (the fetch was truncated, e.g. by rate limiting, and reconciliation was skipped), 'auth_required' (run 'substack-saved-mcp login'), or 'failed'. Requires an active authenticated Substack session.
| Name | Required | Description | Default |
|---|---|---|---|
| force | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| id | No | |
| entity | No | |
| status | Yes | |
| sync_mode | No | |
| started_at | Yes | |
| completed_at | No | |
| error_message | No | |
| fetched_count | No | |
| upserted_count | No | |
| reconciled_count | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does so well: it discloses the early-stop behavior of incremental mode, that force soft-deletes local notes no longer saved remotely, the enumerated SyncRun statuses and their meanings, the truncation/rate-limit case, and the required authenticated session with the login remediation command.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loads the core action and mode distinction, and every sentence adds real information. The run-on enumeration of statuses is dense but each item earns its place; minor structural tightening would help.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Complete for a sync tool: auth requirement, both modes, side effects, and failure/partial outcomes are all covered, and although an output schema exists the status enumeration gives an agent what it needs to interpret results.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0% and the single parameter has no description in the schema, so the description must compensate. It does: it defines the default (incremental, early stop) and the full semantics of force=True, including the soft-delete side effect.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (resync) and resource (saved notes from Substack into local SQLite cache), with an explicit scope of incremental vs full. An agent can distinguish this from sync_saved_posts and the other siblings without opening the schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Clearly explains the default incremental mode and when to use force=True (full list plus reconciliation/soft-delete). It does not explicitly name an alternative sibling tool, but the mode-selection guidance is explicit enough for correct invocation.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
sync_saved_postsA
Trigger incremental or full resync of saved posts from Substack account into local SQLite cache.
Incremental (default) stops early once it reaches already-cached posts, so older posts' reading progress isn't refreshed. force=True fetches the whole list, refreshes every post, and soft-deletes local posts no longer saved on Substack. Returns a SyncRun whose status is 'success', 'partial' (the fetch was truncated, e.g. by rate limiting, and reconciliation was skipped), 'auth_required' (run 'substack-saved-mcp login'), or 'failed'. Requires an active authenticated Substack session and can take minutes on large accounts.
| Name | Required | Description | Default |
|---|---|---|---|
| force | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| id | No | |
| entity | No | |
| status | Yes | |
| sync_mode | No | |
| started_at | Yes | |
| completed_at | No | |
| error_message | No | |
| fetched_count | No | |
| upserted_count | No | |
| reconciled_count | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations exist, so the description carries the full burden and does so well: it discloses the soft-delete of posts no longer saved on Substack under force, the auth prerequisite plus remediation ('substack-saved-mcp login'), the rate-limiting/truncation behavior reflected in 'partial', and that runs can take minutes. These are exactly the behavioral traits an agent needs and none are derivable from structured fields.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The core action is front-loaded in the first sentence, followed by mode behavior, return statuses, and prerequisites in a logical order. It is on the longer side and the status enumeration is verbose, but nearly every sentence earns its place given the mutation semantics.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Even though an output schema exists (so return values need not be documented), the description still names the SyncRun status values and their meanings, which helps the agent interpret partial/auth failures. Combined with the covered prerequisites and side effects, nothing essential for correct invocation is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0% and the single parameter has no schema description, so the description must compensate. It fully explains both values of force: default incremental stops early and leaves older reading progress stale, while force fetches the whole list, refreshes every post, and soft-deletes removed ones. That is richer meaning than the schema's bare boolean-plus-default.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (resync/sync) and resource (saved posts from Substack into local SQLite cache). The resource is precise enough to distinguish it from the sibling sync_saved_notes and from the read-oriented list_saved_posts. An agent can identify the operation without opening the schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly frames the two modes and when to pick each: incremental is the default that stops at already-cached posts, force=True refreshes everything. It does not, however, contrast usage against sibling tools (e.g. when to sync vs. read), so it stops short of the alternative-routing that a 5 requires.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
unsave_noteA
Unbookmark a Substack note remotely and soft-delete it in local cache.
Soft-deletion preserves note history while removing it from active search/list outputs. remote_confirmed=False means the note was still soft-deleted locally, but the tool could not verify the unbookmark on Substack's side.
| Name | Required | Description | Default |
|---|---|---|---|
| url_or_id | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden of behavioral disclosure, and it delivers: it explains the soft-delete side effect, what's preserved (note history), what's affected (active search/list outputs), and the meaning of remote_confirmed=False as a partial-failure state. It could add idempotency behavior or error handling, preventing a 5, but this is well above the minimum.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three sentences, front-loaded core purpose, with each subsequent sentence adding meaningful behavior about soft-deletion and the remote_confirmed flag. Every word earns its place; no fluff or redundancy. This is textbook conciseness without under-specification.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a single-parameter, low-complexity tool with an output schema, the description covers the essentials: remote action, local cache behavior, and a partial-failure state. Since an output schema exists, the description needn't explain return values, though it could have addressed edge cases like calling on an already-unsaved note or transient network errors.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0% and the description never explicitly explains the url_or_id parameter format or accepted values. However, the parameter name is largely self-descriptive, and the description's mention of remote_confirmed indirectly hints at the identifier's role in the unbookmark operation. A bit more explicit guidance (URL formats, prefix expectations) would justify a higher score.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description uses a specific verb+resource+scope: 'Unbookmark a Substack note remotely and soft-delete it in local cache.' It clearly distinguishes from sibling unsave_post by scoping to notes and explicitly describes the dual remote/local action. This matches the 'specific verb+resource+scope' high bar.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The usage context is clearly implied (use when you want to remove a bookmark from a note), but no explicit when/when-not rules or named alternatives are given. Unlike the calibration example that says 'use search_calls_extensive instead,' this description doesn't guide the agent to an alternative sibling, so it earns a 3 for implied usage without exclusions.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
unsave_postA
Unbookmark a Substack post remotely and soft-delete it in local cache.
Soft-deletion preserves post history while removing it from active search/list outputs. When the post's Substack ID is known (normally true after a sync), this calls Substack's real unsave endpoint directly and is reliably confirmed; otherwise it falls back to a best-effort DOM click (see save_post). remote_confirmed=False means the post was still soft-deleted locally, but the tool could not verify the unbookmark on Substack's side.
| Name | Required | Description | Default |
|---|---|---|---|
| url_or_id | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description fully carries the behavioral disclosure burden. It explains soft-deletion semantics, that remote_confirmed=False indicates local deletion without remote verification, and describes the direct endpoint vs. best-effort DOM click fallback. This is transparent and detailed.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is information-dense with no filler. The main action is front-loaded, and each subsequent sentence adds essential detail about soft-deletion, remote confirmation, and fallback behavior. The length is justified by the tool's nuanced behavior.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Despite having only one parameter and an output schema, the description covers the tool's core behavior, failure mode, and local side effects. It explains the remote_confirmed field and points to save_post for the fallback, making the description sufficiently complete for an agent to invoke the tool correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema provides only a parameter name 'url_or_id' with no description, and the description does not explicitly define accepted formats. However, the description does add useful context about using the Substack ID when known and the fallback behavior, which partially compensates for the 0% schema coverage.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a specific verb and resource: 'Unbookmark a Substack post remotely and soft-delete it in local cache.' It clearly distinguishes this from sibling tools like save_post and list_saved_posts by describing both the remote action and local cache behavior.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides clear context on when to use the tool—such as when the post's Substack ID is known after a sync—and references save_post for the fallback DOM-click behavior. However, it does not explicitly state when not to use this tool or compare it to alternatives like delete or list operations.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
4 tool updates
v0.3.1- Changed
get_saved_post1 field changed- changed
Output schema / properties / result / anyOfPrevious value: -[ - { - "description": "Represents a Substack post record stored in SQLite.", - "properties": { - "audience": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null - }, - "author_name": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null - }, - "content_text": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null - }, - "created_at": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null - }, - "excerpt": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null - }, - "id": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "default": null - }, - "image_url": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null - }, - "is_paywalled": { - "default": 0, - "type": "integer" - }, - "is_saved": { - "default": 1, - "type": "integer" - }, - "publication_name": { - "type": "string" - }, - "publication_url": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null - }, - "published_at": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null - }, - "reading_time_minutes": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "default": null - }, - "saved_at": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null - }, - "substack_post_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null - }, - "title": { - "type": "string" - }, - "unsaved_at": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null - }, - "updated_at": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null - }, - "url": { - "type": "string" - }, - "word_count": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "default": null - } - }, - "required": [ - "url", - "title", - "publication_name" - ], - "type": "object" - }, - { - "type": "null" - } -]New value: +[ + { + "description": "Represents a Substack post record stored in SQLite.", + "properties": { + "audience": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null + }, + "author_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null + }, + "content_text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null + }, + "excerpt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null + }, + "id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null + }, + "image_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null + }, + "is_fully_read": { + "readOnly": true, + "type": "boolean" + }, + "is_paywalled": { + "default": 0, + "type": "integer" + }, + "is_saved": { + "default": 1, + "type": "integer" + }, + "is_viewed": { + "default": 0, + "type": "integer" + }, + "max_read_progress": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null + }, + "minutes_remaining": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "readOnly": true + }, + "publication_name": { + "type": "string" + }, + "publication_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null + }, + "published_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null + }, + "read_progress": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null + }, + "reading_time_minutes": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null + }, + "saved_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null + }, + "substack_post_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null + }, + "title": { + "type": "string" + }, + "unsaved_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null + }, + "updated_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null + }, + "url": { + "type": "string" + }, + "word_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "url", + "title", + "publication_name", + "is_fully_read", + "minutes_remaining" + ], + "type": "object" + }, + { + "type": "null" + } +]
- Changed
list_saved_posts7 fields changed- added
Input schema / properties / read_stateAdded value: +{ + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null +} - added
Output schema / properties / result / items / properties / is_fully_readAdded value: +{ + "readOnly": true, + "type": "boolean" +} - added
Output schema / properties / result / items / properties / is_viewedAdded value: +{ + "default": 0, + "type": "integer" +} - added
Output schema / properties / result / items / properties / max_read_progressAdded value: +{ + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null +} - added
Output schema / properties / result / items / properties / minutes_remainingAdded value: +{ + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "readOnly": true +} - added
Output schema / properties / result / items / properties / read_progressAdded value: +{ + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null +} - changed
Output schema / properties / result / items / requiredPrevious value: -[ - "url", - "title", - "publication_name" -]New value: +[ + "url", + "title", + "publication_name", + "is_fully_read", + "minutes_remaining" +]
- Changed
saved_posts_status4 fields changed- added
Output schema / properties / minutes_remaining_totalAdded value: +{ + "default": 0, + "type": "integer" +} - added
Output schema / properties / posts_fully_readAdded value: +{ + "default": 0, + "type": "integer" +} - added
Output schema / properties / posts_in_progressAdded value: +{ + "default": 0, + "type": "integer" +} - added
Output schema / properties / posts_unreadAdded value: +{ + "default": 0, + "type": "integer" +}
- Changed
search_saved_posts7 fields changed- added
Input schema / properties / read_stateAdded value: +{ + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null +} - added
Output schema / properties / result / items / properties / is_fully_readAdded value: +{ + "readOnly": true, + "type": "boolean" +} - added
Output schema / properties / result / items / properties / is_viewedAdded value: +{ + "default": 0, + "type": "integer" +} - added
Output schema / properties / result / items / properties / max_read_progressAdded value: +{ + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null +} - added
Output schema / properties / result / items / properties / minutes_remainingAdded value: +{ + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "readOnly": true +} - added
Output schema / properties / result / items / properties / read_progressAdded value: +{ + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null +} - changed
Output schema / properties / result / items / requiredPrevious value: -[ - "url", - "title", - "publication_name" -]New value: +[ + "url", + "title", + "publication_name", + "is_fully_read", + "minutes_remaining" +]
9 tool updates
v0.3.0- Added
get_note_content - Added
get_saved_note - Added
list_saved_notes - Added
save_note - Changed
saved_posts_status4 fields changed- added
Output schema / properties / last_note_sync_statusAdded value: +{ + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null +} - added
Output schema / properties / last_successful_note_syncAdded value: +{ + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null +} - added
Output schema / properties / total_saved_notesAdded value: +{ + "default": 0, + "type": "integer" +} - added
Output schema / properties / total_unsaved_notesAdded value: +{ + "default": 0, + "type": "integer" +}
- Added
search_saved_notes - Added
sync_saved_notes - Changed
sync_saved_posts1 field changed- added
Output schema / properties / entityAdded value: +{ + "default": "post", + "type": "string" +}
- Added
unsave_note
10 tool updates
v0.1.0- First observed
get_post_content - First observed
get_saved_post - First observed
list_audiences - First observed
list_publications - First observed
list_saved_posts - First observed
save_post - First observed
saved_posts_status - First observed
search_saved_posts - First observed
sync_saved_posts - First observed
unsave_post
TDQS
Scored across 17 tools
Posts and notes have parallel tool sets, and most operations are distinct (list, search, sync, save, unsave). However, get_saved_post vs get_post_content and get_saved_note vs get_note_content overlap in retrieving post/note data, which could cause misselection.
Most tools use a verb_noun pattern (list_saved_posts, save_post, sync_saved_notes), but the 'saved_' prefix is applied inconsistently: save_post/unsave_post/get_post_content omit it while list_saved_posts/get_saved_post include it. saved_posts_status also breaks the verb_noun pattern.
17 tools cover two entity types (posts, notes) with parallel lifecycle operations plus three shared utilities; each tool has a clear role. Slightly heavy but reasonable for the domain.
Core lifecycle for saved posts and notes is covered: list, get, search, save, unsave, sync, and content fetch. Minor gap: no tool to manually update read state/progress despite read_state filters, and saved_posts_status is named for posts only.
Maintenance
Related MCP Connectors
Agent-native MCP server over the public saagarpatel.dev corpus. Read-only, stateless.
Search Hacker News, Bluesky, and Substack from a single MCP interface
Remote MCP server for supportsheep: run AI interviews and manage support content for your blog.
Personal knowledge base MCP server with semantic search, auto-categorization, metadata extraction
Related MCP Servers
- AlicenseAqualityBmaintenanceStandalone Substack CLI + 26-tool MCP server. Your IDE drafts the replies. Zero AI API keys.261MIT
- AlicenseNot gradedqualityDmaintenanceMCP server for Substack that enables reading articles, comments, feed, and subscriptions from AI clients like Cursor and Claude, with optional authentication for paid content.19 npm3MIT
- AlicenseBqualityDmaintenanceAn MCP server that gives Copilot access to Substack posts from your authenticated reader account, including paid subscriptions, enabling daily summary generation by syncing, caching, and summarizing posts.13MIT
- AlicenseNot gradedqualityDmaintenancePrivate MCP server for validating, previewing, creating, and updating Substack newsletter drafts through an MCP-compatible client.403 npm1Apache 2.0