Skip to main content
Glama

substack-mcp

Create and manage your Substack newsletter from your AI assistant or terminal. Prepare rich drafts, search your archive, publish Notes, inspect analytics, and manage explicitly consented free subscribers across publications. Review and publish long-form posts in Substack.

License: MIT Language: TypeScript npm version MCP HOL Plugin Security Scan Podcast X


Create, search, export, plan and review a draft with substack-mcp

The demo runs actual MCP handlers against offline sample data. No live API calls or publication occur. Follow the draft workflow to create, find, export and review a post.

Safe by design — with one loud exception: This server cannot publish or delete long-form posts. Post tools create and edit drafts only; you review and publish manually through Substack's editor. The exception is Substack Notes: create_note and create_note_with_link publish short-form Notes immediately, because Notes have no draft state on Substack. Treat the Note tools as public-publish actions — there is no preview step and no undo from this server. The split is proportionate review, the piece of trust infrastructure for agents this server cares most about: the high-stakes surface gets a human gate, and the exception is stated loudly.

This server imposes no Bestseller-status check. Use an authenticated account with permission to manage the publication; individual operations depend on your Substack access. Connect through local stdio or self-hosted HTTP.

For 1.0 setup coverage and account eligibility, see compatibility. Maintainers can use the release checklist and distribution inventory.

Contents: Quick start · Setup · Tools · Operator CLI · Draft workflow · Analytics to draft · Export · Markdown · Multiple publications · Transports · Compatibility

Quick start

  1. Install and sign in. Browser login needs the optional Playwright dependency:

    npm install @conorbronsdon/substack-mcp playwright
    npx playwright install chromium
    npx substack-mcp login https://yourblog.substack.com --user-id 12345

    Use your own account's user ID (how to find it). The session is saved only after a bounded authenticated read succeeds. To paste credentials instead, see Option B.

  2. Verify a read. npx substack-mcp doctor --json --check-auth makes one bounded read per publication. It confirms read access, not your user ID or write permission.

  3. Connect your MCP client. Add the server to Claude Desktop or Claude Code, or use the Codex plugin. Environment variables take precedence; omit them to use the stored browser-login session. Then ask your assistant: "How many Substack subscribers do I have?"

  4. Prepare a draft for review. Follow the draft workflow: create_draft, search_posts, export_draft, preflight_draft, then plan_draft_update and update_draft. Long-form posts stay unpublished drafts until you publish them in Substack's editor. create_note and create_note_with_link publish immediately.

Related MCP server: substack-mcp

Community walkthrough

Jonathan Price's I used Codex to connect ChatGPT to Substack. Then it drafted this post. walks through using Codex to install the MCP locally, connecting ChatGPT through OpenAI's Secure MCP Tunnel, and creating a private draft for manual publication. He used the connection to create the draft of the guide itself.

The guide documents his September 18, 2026 setup with version 1.2.0. Its reported get_post 404 is fixed in 1.2.1. Client interfaces and access requirements can change; use the setup instructions below for this package's current configuration. This is a community walkthrough, not a hosted service provided by this project.

Setup

Requires Node.js 22 or newer (CI covers Node 22 and 24). Browser login additionally requires Playwright.

You can supply credentials two ways: paste them as env vars (below), or run the optional browser login which captures and stores them for you.

Install the server and optional Playwright dependency together in a local tools directory, then sign in:

npm install @conorbronsdon/substack-mcp playwright
npx playwright install chromium
npx substack-mcp login https://yourblog.substack.com --user-id 12345

substack-mcp-login remains a supported alias. Missing publication URL and user ID are prompted. Supply your own account's user ID (steps below); a post author's byline does not verify your identity. The browser opens for sign-in, including any CAPTCHA. Only a cookie applicable to the publication API is captured, and a bounded authenticated read must succeed before saving. This verifies read access, not the configured user ID or permission to write.

Without --profile, login saves ~/.substack-mcp/session.json (directory override: SUBSTACK_MCP_HOME). The server uses this legacy session when publication credential environment variables and SUBSTACK_PROFILES are unset.

Default storage (SUBSTACK_CREDENTIAL_STORE=file): sessions use AES-256-GCM with a key derived from the OS account and machine. File permissions request 0600; Windows access also depends on directory ACLs. This is a machine-bound file, not an OS keychain or secret vault. Code running as your OS user can derive the key. Use environment credentials if your MCP client manages secrets for you.

Optional OS keychain: set SUBSTACK_CREDENTIAL_STORE=keychain in both the login process and the MCP client's environment. macOS uses Keychain via /usr/bin/security; Linux needs libsecret and secret-tool plus an unlocked Secret Service; Windows uses Credential Manager through PowerShell's PasswordVault. All three are exercised with synthetic credentials by the keychain CI workflow on GitHub-hosted runners (an unlocked temporary macOS keychain and a gnome-keyring session on Linux); desktop setups with locked keychains may still prompt. Login writes the selected account to the keychain, and the server reads it there. Explicit keychain selection never reads the encrypted file as a fallback. The keychain helps against other OS users, copied disks, and some malware limited to file access. Code running as your user can usually query the keychain. Keep the OS account and running code trusted. On Linux, secret-tool lookup can exit 1 without an error message for either an absent entry or a locked keyring. Named writes without --force search and unlock first, and refuse overwrites when an entry is found.

Named profiles and migration

npx substack-mcp login https://yourblog.substack.com --user-id 12345 --profile work
npx substack-mcp profiles list
# Copy an existing legacy session without changing its file:
npx substack-mcp profiles migrate --name personal
# Copy a file session or named file profile into the keychain; source remains:
npx substack-mcp profiles migrate --to keychain
npx substack-mcp profiles migrate --to keychain --name work

Keys start with a lowercase ASCII letter and contain only lowercase letters, digits and hyphens, up to 64 characters. Existing profiles require explicit --force to replace. List output contains keys, readability status, publication origins and file save times; it excludes cookies and user IDs. Unreadable profiles remain visible but cannot be selected. Save time records local persistence, including migration; it is not token issuance or expiration time. Listing is bounded to 32 profiles.

Profile storage requires a local filesystem supporting hard links (such as NTFS or a typical Linux filesystem), so creation can install a complete encrypted file without overwriting an existing name. FAT/exFAT and some network mounts are not supported: set SUBSTACK_MCP_HOME to a suitable local directory. Do not use --force to work around an unsupported filesystem.

Set SUBSTACK_PROFILES=work,personal in your MCP client's environment to select up to 32 distinct profiles. Remove all publication credential variables first: combining profile selection with legacy or named credential variables is an error, including empty variables. Missing, corrupt or invalid selected profiles stop startup; they never fall back to another account. Profiles on disk are never activated by discovery. With multiple profiles, tools require an explicit publication key and CLI reads require --publication.

To roll back, unset SUBSTACK_PROFILES and restore your previous environment configuration. Migration preserves the legacy session byte-for-byte. These files use the existing encryption format. profiles list lists file profiles; select keychain profiles explicitly with SUBSTACK_PROFILES after migration or login. Run substack-mcp status --json for offline configuration diagnostics or substack-mcp doctor --check-auth --json for a bounded read per selected account.

Option B — Get your credentials manually

Open your Substack in a browser, then:

  1. Session token: Navigate to your publication, open DevTools → Application → Cookies → copy the value of connect.sid (URL-encoded string starting with s%3A)

  2. User ID: Follow Find your account user ID. Do not use a publication post's byline ID: publications can have multiple authors. This server does not independently verify the supplied ID.

  3. Publication URL: Your Substack URL, including custom domain if you have one (e.g., https://newsletter.yourdomain.com or https://yourblog.substack.com)

Find your account user ID

  1. In Substack, open your own account profile and copy its handle (the part after @ in https://substack.com/@your-handle). A publication URL or another author's profile is not your account handle.

  2. Open https://substack.com/api/v1/user/your-handle/public_profile with that handle substituted. Read the top-level numeric id in the JSON response; do not use an ID nested under a publication or post byline. This public profile endpoint is also used by this server's anonymous profile lookup.

  3. Confirm that the response's handle and name match the account you will sign in with. Supply the top-level id to login --user-id or SUBSTACK_USER_ID. A public profile lookup identifies the handle you entered; the browser-login read check does not prove that the cookie belongs to that ID. Recheck the account before any write operation.

2. Configure your MCP client

Claude Desktop

Add to your claude_desktop_config.json:

{
  "mcpServers": {
    "substack": {
      "command": "npx",
      "args": ["-y", "@conorbronsdon/substack-mcp"],
      "env": {
        "SUBSTACK_PUBLICATION_URL": "https://yourblog.substack.com",
        "SUBSTACK_SESSION_TOKEN": "your-session-token",
        "SUBSTACK_USER_ID": "your-user-id"
      }
    }
  }
}

Claude Code and Codex plugin setup is available alongside manual MCP configuration. The repository marketplace and local plugin are separate from curated-directory acceptance or hosted ChatGPT support.

Claude Code

Add to your .mcp.json:

{
  "mcpServers": {
    "substack": {
      "command": "npx",
      "args": ["-y", "@conorbronsdon/substack-mcp"],
      "env": {
        "SUBSTACK_PUBLICATION_URL": "https://yourblog.substack.com",
        "SUBSTACK_SESSION_TOKEN": "your-session-token",
        "SUBSTACK_USER_ID": "your-user-id"
      }
    }
  }
}

3. Verify

Ask your AI assistant: "How many Substack subscribers do I have?"

Tool output compatibility, response limits and versioning are documented in the tool contract.

Operator diagnostics

The CLI exposes operator commands and configuration checks through the same MCP handlers and credential resolution as the server. drafts create writes one private draft; the other operator commands read:

substack-mcp status --json
substack-mcp doctor --json --check-auth
substack-mcp drafts list --limit 10

Commands, the JSON envelope, exit codes, request deadlines and response limits are documented in operator CLI and diagnostics.

Tools

Every tool declares explicit MCP side-effect annotations; see tool annotations. Tool descriptions carry the authoritative wording.

Read

Tool

Description

get_subscriber_count

Get your publication's current subscriber count

list_subscribers

Read a bounded page of private subscriber records

search_subscribers

Filter and page private subscriber records; optional activity, date, flags and revenue fields

get_subscriber

Look up membership by exact email; reconcile pending additions

list_published_posts

List published posts with pagination

get_publication

Read projected publication identity/settings, verify the configured host, and report missing fields; does not verify account identity or role

list_publication_tags

Read tag definitions, including hidden tags by default, with bounded local pagination

get_post_tags

Resolve post tag associations; preserves unresolved IDs and reports empty-result identity uncertainty. Draft coverage is currently live-verified only for empty responses

search_posts

Search a publication archive by query and status; bounded pages with continuation metadata

plan_draft_update

Review proposed changes, preflight and a receipt for best-effort stale detection; no writes

export_draft

Editable Markdown, exact original body, conversion diagnostics, preflight and editor link

preflight_draft

Read-only checks for title, audience, body structure, images, and paywalls, with an editor link

list_drafts

List draft posts

get_post

Get full content of a published post by ID

get_draft

Get full content of a draft by ID

get_post_comments

Get comments on a published post

get_sections

List your publication's sections (categories) with their IDs

get_post_analytics

Get a published post's stats (views, opens, signups, subscribes, reactions) by ID

rank_posts

Rank posts by views, opens, sends, rates, signups, subscribes, estimated value or date, keeping null and missing values distinct

get_publication_stats

Read dashboard summary and ranged publication metrics, with missing and unavailable states

get_growth_sources

Read bounded growth source attribution and optional events

list_scheduled_posts

List posts scheduled for future publication (read-only; scheduling stays in Substack's editor)

get_user_profile

Read a minimal public user profile by handle, anonymously

get_profile_feed

Read one public profile feed page with cursor continuation, anonymously

get_note_thread

Read a public Note, ancestors and one replies page, anonymously

list_public_posts

Read a bounded public archive page, anonymously

get_public_post

Read an anonymous public post by URL with body status and truncation flags

Public reading

These five tools use a separate anonymous reader. It sends only User-Agent and Accept, never the configured publication cookie or other credentials. The configured publication selects the default archive origin; callers may supply an allowlisted HTTPS publication origin. Allowed hosts are substack.com, one-label *.substack.com, configured publication origins, and exact origins in SUBSTACK_PUBLIC_READ_ORIGINS (comma-separated HTTPS origins without ports, paths or userinfo). Redirects are rejected. A *.substack.com publication may redirect to its custom domain (for example, lenny.substack.com); JSON reads do not follow that redirect. Add the custom HTTPS origin to SUBSTACK_PUBLIC_READ_ORIGINS and read through that origin. With multiple publications, the publication key remains required for every tool.

Profile feed pages are upstream-sized; has_more: null means the upstream omitted continuation metadata. Thread pages can omit replies when more_branches or next_cursor is present; completeness: "unknown" means continuation metadata was omitted. Archive full pages have has_more: null because no total is returned. Public post body_status is a heuristic based on audience and body presence; it does not establish full access. The anonymous reader does not use subscription entitlements. Reader subscriptions and inbox are unsupported: the configured publication session received 401 on the substack.com reader-account routes, which require a separate reader session this server does not manage.

Archive search and draft review

search_posts accepts query (1–500 characters), status (published, drafts, or scheduled, default published), offset (default 0), and limit (1–50, default 25). It makes one authenticated archive request and returns projected metadata, returned, total, has_more, and next_offset. Continue with next_offset and the same query/status. When Substack omits the total and a page is full, has_more is null (unknown); another page may be empty. Substack controls matching and indexing: this is not a guaranteed full-text scan. Use get_post or get_draft to retrieve full content. Pagination is not a snapshot; concurrent edits can move results between pages.

preflight_draft accepts draft_id, reads it once, and returns checks_passed, findings with severity/code/message, content counts and an editor link. It checks title, audience, JSON/body shape, image wrappers and HTTPS sources, and paywall count and edge placement. Unknown nodes and external images produce review warnings. Bodies over two million characters, 10,000 nodes, or depth 100 are not fully checked; counts.complete is false and aggregate checks are skipped after a scan limit. Unknown-node warnings name up to five types for editor review. This is a focused static check, not full ProseMirror validation or publish approval. It does not fetch links/images, verify access settings, or prove final rendering. Review the draft in Substack; no content is modified.

Both tools require publication when multiple publications are configured.

Write (private drafts; image upload returns a public URL)

Tool

Description

create_draft

Create a new draft from markdown (private)

update_draft

Apply a reviewed change receipt; recheck unpublished state and report readback outcomes

update_draft_tags

Plan or change draft tags; dry-run by default, draft-only, with one readback after writes

upload_image

Upload an image to Substack's CDN from a file, data URI or public HTTPS URL — returns a publicly-fetchable (unlisted) URL

Review before changing a draft

In 0.9, call plan_draft_update, review its output, then call update_draft with the same fields and returned receipt. Published or known stale drafts are rejected. The read/write race remains; check readback outcomes and review in Substack. The CLI shares this flow through drafts plan and drafts apply. See draft changes and migration for examples and limits.

Publish (Notes — public immediately)

Tool

Description

create_note

Publish a Substack Note (short-form, publishes immediately)

create_note_with_link

Publish a Note with a link card attachment (publishes immediately)

Notes have no draft state on Substack, so there is no draft-first option for these two tools.

Subscriber management

add_free_subscriber adds one consenting reader to the free newsletter. It is a distribution change: that reader may receive future newsletter emails. It can request a welcome email with send_welcome_email: true (off by default). It never grants paid access or overrides Substack's suppression of previously unsubscribed addresses. Its MCP annotations identify it as an external write (readOnlyHint: false, openWorldHint: true).

{"email":"reader@example.org","consent_confirmed":true,"consent_evidence":{"source":"booking:message-id","recorded_at":"2026-09-01T00:00:00Z"},"dry_run":true}

Dry-run is the default. After checking actual newsletter consent, set dry_run: false to execute. Live adds require the source reference and timestamp in consent_evidence; this attestation is echoed with the publication key for auditing and does not replace checking the underlying consent record. Multi-publication configurations also require the publication selector, just like every other tool.

Results distinguish existing, dry_run, verified, blocked, and unverified, busy, and retryable. busy performs no write; wait for the other operation. retryable means authentication or rate limiting refused the request; resolve that condition before explicitly retrying. An empty API acknowledgement is not proof of addition. verified means an exact membership lookup succeeded after the request; it does not prove that this request originally created the membership. Dashboard data can lag. A missing reader may also have previously unsubscribed.

For unverified, recheck with get_subscriber; never automatically repeat the add. For blocked, review in Substack without bypassing suppression. Automated callers must persist an attempt ledger before sending each live request. The client's in-memory duplicate guard does not survive restarts or separate HTTP sessions. Keep subscriber identities and consent evidence out of shared repositories, prompts to unapproved public services, and routine logs.

Implementation and live verification notes: subscriber API.

For Google Calendar booking opt-ins, the calendar sync helper provides a bounded Gmail scan, latest-answer selection, a private durable attempt ledger, and read-only reconciliation after uncertain writes. Scheduling is an explicit local setup step; installing the MCP does not start a background job.

Intentionally excluded

  • Publish posts — Publishing long-form posts should be a deliberate human action (Notes are the documented exception above)

  • Delete — Too destructive for an AI tool

  • Schedule — Use Substack's editor for scheduling. (list_scheduled_posts reads what you've queued there, but this server never creates, edits, or cancels a schedule.)

For an always-on scheduler with durable cloud state and weekly email reports, see Cloud Calendar sync.

Multiple publications

Running more than one publication behind a single server? Set a SUBSTACK_PUB_<KEY>_* triplet per publication instead of the plain SUBSTACK_* vars. <KEY> is any name you choose (letters, digits, underscores) — it becomes the publication's lowercase, hyphenated key, e.g. KEVIN_MULDOON → kevin-muldoon.

"env": {
  "SUBSTACK_PUB_KEVIN_MULDOON_PUBLICATION_URL": "https://kevinmuldoon.substack.com",
  "SUBSTACK_PUB_KEVIN_MULDOON_SESSION_TOKEN": "token-1",
  "SUBSTACK_PUB_KEVIN_MULDOON_USER_ID": "111",
  "SUBSTACK_PUB_SAPERE_PUBLICATION_URL": "https://sapere.substack.com",
  "SUBSTACK_PUB_SAPERE_SESSION_TOKEN": "token-2",
  "SUBSTACK_PUB_SAPERE_USER_ID": "222"
}

Each triplet is independent, and setting any SUBSTACK_PUB_<KEY>_* variable declares that publication. An incomplete triplet — a missing variable, an empty value, or a whitespace-only value — fails startup with an error naming the key, rather than silently dropping that publication. That matters because a dropped publication is not "one fewer publication": drop the only one and the server falls back to your stored browser-login session; drop one of two and every tool loses its publication parameter, so a call meant for the dropped publication routes silently to the surviving one.

Keys are compared case-insensitively, with _ folded to -. Two names that resolve to the same key (SUBSTACK_PUB_ALPHA_* and SUBSTACK_PUB_Alpha_*) are a startup error too — merging them silently would let one publication's URL pair with another's session token.

<KEY> accepts ASCII letters, digits, and underscores; the three suffixes must be uppercase and the whole name must have no stray whitespace. Anything that begins with SUBSTACK_PUB_ but does not fit that shape — a hyphen in the key, a lowercase suffix, an accented character, a trailing space — is a startup error naming the variable, not a variable that gets quietly ignored. For the same reason as above: an ignored publication is not one fewer publication, it is a silent reroute to a different one.

With two or more publications configured, every tool gains a required publication parameter — one of your configured keys (e.g. kevin-muldoon, sapere above). The calling model must specify one on every call; an unrecognized value is rejected before any Substack API call is made, so a stray write can't land on the wrong publication. With exactly one publication configured — the common case, whether via plain SUBSTACK_* vars or a single SUBSTACK_PUB_<KEY>_* triplet — no publication parameter is added at all; every tool's schema is unchanged from single-publication mode.

Don't mix the two styles: if any SUBSTACK_PUB_<KEY>_* var is set, the plain SUBSTACK_* vars are ignored (with a startup warning) rather than treated as an unnamed extra publication.

SUBSTACK_USER_AGENT and SUBSTACK_REQUEST_TIMEOUT_MS apply to every configured publication — they are not per-publication. Browser login also supports explicit named profiles; see Named profiles and migration.

Token expiration

Substack session tokens expire periodically (typically ~90 days). If you get authentication errors, grab a fresh connect.sid cookie from your browser and update the env var (make sure ad blockers are disabled when copying the cookie) — or, if you used the browser login, just re-run substack-mcp-login to refresh the stored session.

Custom domains & Cloudflare

This section covers authenticated creator API calls. Anonymous public reading uses the public reading origin rules above.

Substack publications served on a custom domain (e.g. blog.example.com) sit behind Cloudflare, which can reject non-browser requests with 403 error code: 1010. To avoid this, the server sends a browser User-Agent and a Referer by default, and addresses the publication by its canonical *.substack.com host.

  • Use the canonical host. Set SUBSTACK_PUBLICATION_URL to the publication's *.substack.com address rather than the custom domain. Calls to the canonical host are served directly; custom-domain calls may 301-redirect and then 401.

  • Override the User-Agent (optional) via SUBSTACK_USER_AGENT if you need a different browser signature:

"env": {
  "SUBSTACK_PUBLICATION_URL": "https://yourblog.substack.com",
  "SUBSTACK_SESSION_TOKEN": "your-session-token",
  "SUBSTACK_USER_ID": "your-user-id",
  "SUBSTACK_USER_AGENT": "Mozilla/5.0 ..."
}

Request timeout

Every request to Substack is bounded by a 30-second deadline. Node applies no request timeout of its own — only a 10-second connect timeout — so a host that accepts the connection and then goes silent (a proxy that drops packets rather than refusing them) would otherwise hang a tool call indefinitely. A request that hits the deadline fails with a TimeoutError naming the endpoint and the limit.

Raise or lower it with SUBSTACK_REQUEST_TIMEOUT_MS (milliseconds; a non-numeric or non-positive value is ignored with a warning and the default is used):

"env": {
  "SUBSTACK_REQUEST_TIMEOUT_MS": "60000"
}

Transports

By default the server speaks MCP over stdio, which the client configurations above assume. Set MCP_TRANSPORT=http for a stateless Streamable HTTP server (POST /mcp, GET /health) in a persistent, self-hosted deployment. Setup, listener variables and the security model are in HTTP transport.

What this listener will accept

The HTTP listener starts closed: loopback Host and Origin allowlists by default, an optional bearer token (MCP_HTTP_TOKEN) and a 10 MiB body cap. Host and Origin checks are not authentication; set a token wherever other processes can reach the port. See the listener policy.

Versioned GHCR images and transport verification: container distribution.

Typed errors

API failures map to typed errors (AuthenticationError, RateLimitError, ValidationError, NotFoundError, ServerError, TimeoutError, and the base SubstackAPIError). The status mapping and error-body parsing are documented in typed errors.

Draft export

Use export_draft for a read-only Markdown/JSON bundle, or run:

substack-mcp export 42 --output draft-export.json
substack-mcp export 42 --format markdown --output draft.md

Markdown exports retain the exact original body in a .source.json sidecar. Inspect unsupported_nodes before reuse. Existing files require --force. See export and CLI behavior for publication selection, limits, partial exports and file recovery.

Markdown support

Drafts accept CommonMark/GFM Markdown: headings, nested bold/italic/strikethrough, links and reference links, images with captions and linked destinations, nested lists with starting numbers, code, blockquotes, rules and hard breaks. A standalone <!-- paywall --> block adds one paywall to a long-form draft.

Unsupported content returns unsupported_nodes before a write. After reviewing those diagnostics, draft callers can explicitly set allow_unsupported: true to retain literal fallbacks. Tables remain Markdown inside code blocks; native tables, callouts and arbitrary embeds are not advertised as supported. Footnotes in top-level paragraphs map to the editor's native footnotes in long-form drafts. Notes reject unsupported conversion before either publication or attachment creation and have no fallback override.

See Markdown authoring for mappings, limits, compatibility changes and the distinction between offline fixtures and live editor checks.

Important notes

  • This server uses Substack's unofficial API. It may break if Substack changes their endpoints.

  • Session tokens are sent as cookies. Keep your SUBSTACK_SESSION_TOKEN secure.

  • The server checks your credentials on startup, after the MCP handshake completes, and only warns — it never blocks startup on a network call. Tools still error individually if the token is expired, which is where the failure is actionable.

  • SIGTERM and SIGINT are handled: the server closes its transport and exits 0, so docker stop returns promptly instead of waiting out the grace period.

Development

For opt-in live read checks, see live contract evidence. The probe is disabled in ordinary CI and never publishes or writes.

Before releasing, run npm run test:package. It installs the built tarball with production dependencies in a clean temporary directory, checks both executable entrypoints, and verifies the MCP version and the complete registered tool catalog without real credentials.

git clone https://github.com/conorbronsdon/substack-mcp.git
cd substack-mcp
npm install
npm run build

Run locally:

SUBSTACK_PUBLICATION_URL=https://yourblog.substack.com \
SUBSTACK_SESSION_TOKEN=your-token \
SUBSTACK_USER_ID=your-id \
npm start

Contributing

Issues and pull requests are welcome. Because this server uses Substack's unofficial API, the most useful contributions are fixes when an endpoint changes. If a tool stops working, open an issue with the tool name and the error. The safe-by-design boundary stays: no publish, no delete, no schedule for long-form posts. Notes publish immediately by design and must keep saying so loudly in their descriptions.

About

Built and maintained by Conor Bronsdon for the Chain of Thought podcast production workflow, where it drafts and reviews newsletter posts before a human hits publish. Conor hosts Chain of Thought, a show about AI infrastructure and how practitioners actually build with it. More tools for creators live in ai-tools-for-creators. Find Conor on X at @ConorBronsdon.

Companion tools:

  • Transistor MCP: Transistor.fm's official MCP server. Episodes, publishing, and analytics.

  • podcastindex-mcp: search the Podcast Index and track guest appearances

  • op3-mcp: report downloads, listener geography, and apps from OP3

  • apple-podcasts-mcp: pull plays, followers, and per-episode listening from Apple Podcasts Connect

  • gsc-mcp: query search performance, keywords, and sitemaps in Google Search Console

  • podcast-benchmark: benchmark a show against its peers using only public data


Disclaimer

This is an independent personal project, not affiliated with, sponsored by, or endorsed by any company. All views expressed are my own.

Codex plugin

The repository includes a Codex manifest at .codex-plugin/plugin.json and an MCP configuration at .mcp.json. It runs the published npm package over stdio using npx; Node.js and npm must be available. The package version is pinned in .mcp.json, so upgrading the plugin's server is an explicit change.

Configure your Substack credentials outside the plugin using the environment variables or browser-login session described above. Never commit a session token. Installation does not authenticate an account or grant approval to post. Long-form posts remain drafts; Notes publish immediately.

License

MIT

Available Tools

34 tools
add_free_subscriberA

Add one explicitly opted-in reader to this publication's free newsletter. Changes email distribution: future newsletter emails may be delivered. Requires verified newsletter consent; never infer consent from a meeting alone. Dry-run by default; set dry_run=false to write. Set send_welcome_email=true to request Substack's welcome email for a new addition; delivery is not verified. Never grants paid access or overrides suppression. Existing members are skipped. An unverified result MUST be reconciled using get_subscriber, not automatically retried. Automated callers must persist an attempt ledger BEFORE invoking this tool; in-memory duplicate protection does not survive restarts or separate HTTP sessions.

ParametersJSON Schema
NameRequiredDescriptionDefault
emailYes
dry_runNo
consent_evidenceNoRequired for a live add: the source reference and timestamp of this email address's explicit newsletter opt-in. Retain the underlying evidence privately; this field records caller attestation, not independent proof.
consent_confirmedYes
send_welcome_emailNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
noteYes
emailYes
statusYes
subscriberNo
publicationYes
consent_evidenceNo

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description adds substantial behavioral context beyond the annotations: it changes future email distribution, default dry-run behavior, welcome email delivery is unverified, it never grants paid access or overrides suppression, existing members are skipped, and automated callers must persist an attempt ledger before invoking. These are non-obvious effects an agent could not infer from readOnlyHint or destructiveHint alone.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense but every sentence earns its place: it opens with the core operation, then covers side effects, consent requirements, dry-run behavior, failure handling, and cross-session persistence. There is no filler or repetition.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool has an output schema and five parameters including nested consent evidence, the description is complete: it covers prerequisites, side effects, reconciliation, duplicate protection, and non-idempotency. Nothing needed for safe invocation is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is only 20%, and the description compensates by explaining dry_run, send_welcome_email, and the consent requirement. consent_evidence already has its own schema description, and email is self-explanatory. The description therefore adds meaning to the most behavior-critical parameters even though not every parameter is individually discussed.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a precise verb and resource: 'Add one explicitly opted-in reader to this publication's free newsletter.' It also scopes the operation to free access and explicitly says it 'Never grants paid access,' which distinguishes it from any subscription or payment-related sibling tool.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives concrete usage rules: requires verified newsletter consent, never infer consent from a meeting, dry-run by default with dry_run=false to write, and existing members are skipped. It also names the right reconciliation path: 'An unverified result MUST be reconciled using get_subscriber, not automatically retried.'

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

create_draftA

Create a new draft post. Accepts markdown body which is converted to Substack's format. Does NOT publish — creates a draft only.

ParametersJSON Schema
NameRequiredDescriptionDefault
bodyNoPost body in markdown format
titleYesPost title
audienceNoWho can see this posteveryone
subtitleNoPost subtitle
allow_unsupportedNoAcknowledge conversion diagnostics and retain unsupported Markdown literally in this private draft

Output Schema

ParametersJSON Schema
NameRequiredDescription
idYes
titleNo
messageYes
unsupported_nodesYes

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only indicate readOnlyHint=false, destructiveHint=false, and openWorldHint=false. The description adds meaningful behavioral context beyond that: it accepts markdown and converts it to Substack's format, and it creates a draft without publishing. This helps the agent understand side effects and scope without contradicting the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two tight sentences with no filler. The primary action and the most important caveat ('Does NOT publish') are front-loaded, and the markdown conversion detail earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a create tool with five parameters, a full output schema, and annotations covering the safety profile, the description is sufficient: it names the resource, the format conversion, and the non-publishing behavior. It could be slightly more complete by explicitly distinguishing from update_draft, but that gap is minor.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3. The description adds extra meaning to the body parameter ('converted to Substack's format'), which enriches what the schema says. It doesn't detail every parameter, but the schema already covers those well.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific action ('Create a new draft post') on a specific resource, and explicitly distinguishes it from publishing by saying 'Does NOT publish — creates a draft only.' The word 'new' also differentiates it from sibling tools like update_draft and export_draft.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description clearly implies when to use the tool: to create a draft rather than publish. It also states an explicit exclusion ('Does NOT publish'). However, it does not explicitly name alternatives such as update_draft for existing drafts or create_note for notes, so it stops short of full routing guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

create_noteA

Create a Substack Note (short-form content). Accepts markdown text. PUBLISHES IMMEDIATELY to your public Notes feed — Notes have no draft state on Substack, and this server has no delete tools, so there is no undo from here.

ParametersJSON Schema
NameRequiredDescriptionDefault
bodyYesNote content in markdown format

Output Schema

ParametersJSON Schema
NameRequiredDescription
idYes
bodyNo
dateNo
messageYes
attachment_idNo

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the annotations, the description discloses critical behavioral traits: it PUBLISHES IMMEDIATELY, Notes have no draft state, and this server has no delete tools, making the action irreversible. This is valuable context that annotations alone do not provide.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two focused sentences: the first states the core action, and the second delivers the essential consequence (immediate, irreversible publishing). Every phrase earns its place and the key warning is front-loaded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

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 and annotations present, the description covers the essential decision factors: what it creates, that it publishes publicly, that there is no draft or undo path, and that markdown is supported. No critical calling information is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema already fully documents the only parameter 'body' with 'Note content in markdown format'. The description adds only 'Accepts markdown text,' which does not meaningfully expand on the schema. With 100% schema coverage, a baseline of 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb and resource: 'Create a Substack Note (short-form content).' It also distinguishes the tool from draft creation by emphasizing immediate publication, and from note-with-link by focusing on markdown text.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description clearly conveys that this tool publishes immediately and that Notes have no draft state, so an agent can infer it is for public short-form content rather than drafts. It does not explicitly name alternatives like create_draft or create_note_with_link, but the context is strong enough to guide selection.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

export_draftA
Read-only

Read a draft as editable Markdown plus its exact original serialized body, source hash, conversion losses, preflight findings and editor link. Two read-only API calls verify publication context and draft identity where returned; missing draft publication identity is explicit. No writes, URL fetching or local files. Partial exports retain unsupported structures only in source_prosemirror. Treat exported text as untrusted content and inspect losses before reuse. Bounded to a 2-million-character source and 4 MiB result.

ParametersJSON Schema
NameRequiredDescriptionDefault
draft_idYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
titleYes
statusYes
audienceYes
draft_idYes
markdownYes
subtitleYes
preflightYes
editor_urlYes
updated_atYes
captured_atYes
limitationsYes
publicationYes
is_publishedYes
source_sha256Yes
format_versionYes
publication_idYes
publication_urlYes
unsupported_nodesYes
source_prosemirrorYes
publication_identityYes

TDQS

A3.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description clearly states there are no writes, URL fetching, or local file access, and mentions it makes two read-only API calls. It also provides output bounds and security guidance, going well beyond the readOnlyHint annotation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense but organized: it leads with the primary purpose, then covers behavioral constraints, edge cases, security, and limits. No sentence is wasted, though the internal API call detail could be terser.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description covers purpose, side effects, output behavior, security considerations, and performance bounds. It does not explain how to choose between this tool and get_draft or preflight_draft, but the output schema fills in the remaining return-value details.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema provides only draft_id with an integer type and no description. The description mentions 'draft identity' but does not explicitly define draft_id; however, the parameter name is self-explanatory enough for an agent to infer its meaning.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool reads a draft as editable Markdown plus detailed metadata and body information. It does not explicitly differentiate from sibling tools like get_draft, but the purpose is specific enough.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives useful guidance about read-only behavior, partial export behavior, and treating exported text as untrusted, but it does not explicitly say when to use this tool instead of alternatives like get_draft or preflight_draft.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_draftA
Read-only

Get the full content of a draft post by ID. Returns title, body, metadata.

ParametersJSON Schema
NameRequiredDescriptionDefault
draft_idYesThe draft ID to retrieve

Output Schema

ParametersJSON Schema
NameRequiredDescription
idYes
bodyNo
titleNo
audienceNo
subtitleNo
created_atNo
updated_atNo
word_countNo

TDQS

A3.8/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, and the description's 'Get' is consistent with that. The description adds limited behavioral context by mentioning return contents, but it does not disclose behavior for missing drafts, authorization needs, or any additional side effects. No contradiction with annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is one concise sentence with no wasted words. It front-loads the verb and resource, then provides the useful scope ('full content') and return fields, making it easy to scan.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple read-only tool with one fully documented required parameter and an output schema present, the description is almost complete. It lacks an explicit pointer about when to choose this over closely related siblings like export_draft or list_drafts, but that gap is minor given the clarity of the rest.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema has 100% description coverage for the single parameter, already stating 'The draft ID to retrieve'. The description's 'by ID' adds no new semantic meaning beyond the schema, so the baseline of 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the specific verb ('Get'), resource ('draft post'), and selection criterion ('by ID'), and further specifies 'full content' with return fields ('title, body, metadata'). This distinguishes it from siblings like list_drafts, get_post, and export_draft.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies when to use the tool: when the agent has a draft_id and needs the full content of a specific draft. However, it does not explicitly name alternatives or exclusions, such as when to prefer list_drafts or export_draft, so guidance is only implicit.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_growth_sourcesA
Read-only

Read growth sources for an ordered inclusive date range of at most 366 days ending no later than tomorrow UTC. One authenticated read, or two when include_events is true; no writes. Optional events report available items or an unavailable reason without discarding sources; authentication failure still stops the call. Returns up to 20 top-level sources by default, at most 50, in Substack's users-descending order. Processes at most 500 nodes, depth 3 and 400 timeseries points per metric; truncation flags identify cut data. total_sources and has_more describe only the unpaginated response's top-level array, not all upstream sources or complete attribution.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNo
to_dateYes
from_dateYes
include_eventsNo
include_timeseriesNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
eventsNo
totalsYes
sourcesYes
to_dateYes
has_moreYes
returnedYes
from_dateYes
truncatedYes
publicationYes
total_sourcesYes

TDQS

A4.2/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description goes well beyond the readOnlyHint annotation. It discloses the number of authenticated reads (1 or 2 with include_events), explicitly states 'no writes', and details edge cases: authentication failure stops the call, truncation flags identify cut data, and the scope of total_sources/has_more. It also explains the processing limits (500 nodes, depth 3, 400 timeseries points) and the default/max return count. This is rich behavioral context that the annotation alone does not provide.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single dense paragraph that front-loads the core purpose and constraints before delving into details. Every sentence carries information – no filler. It could be improved with bullet points for readability, but it is appropriately sized given the number of behavioral details it conveys. It is efficient and structured logically: purpose → read count → events → limits → truncation.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The tool has an output schema, so the description need not explain return values. Given that, the description is remarkably complete: it covers authentication, read counts, date range constraints, pagination/limit behavior, event behavior, truncation flags, and the scope of summary fields. It even notes that authentication failure stops the call. There is no obvious missing information an agent would need to call this correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With schema description coverage at 0%, the description must compensate. It explains from_date/to_date via 'ordered inclusive date range', limit via 'up to 20 top-level sources by default, at most 50', and include_events via 'two when include_events is true' and the optional events report. However, include_timeseries is not explicitly described – the mention of '400 timeseries points per metric' hints at it but does not clarify the parameter's role. The description covers most parameters but leaves include_timeseries implicit, so it only partially compensates for the missing schema descriptions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with 'Read growth sources' – a specific verb and resource – and adds a precise scope (ordered inclusive date range, ≤366 days, ending ≤tomorrow UTC). This distinguishes it from sibling tools like get_publication_stats or get_post_analytics, which target different metrics. The 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.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

While the description clearly states what the tool does and its constraints (date range, limits, events), it does not explicitly tell the agent when to choose this over alternatives, nor does it mention any sibling tools or exclusions. The agent must infer that growth sources are distinct from analytics or subscriber tools. There is no 'use this instead of X when...' guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_note_threadA
Read-only

Anonymous public Note thread read, no credentials sent. Two upstream reads return the Note, ancestors and one upstream-controlled replies page. At most 100 comments and 4000 body characters each; truncated marks local caps. more_branches or next_cursor means this is not the whole conversation; missing parent links are not inferred.

ParametersJSON Schema
NameRequiredDescriptionDefault
cursorNo
comment_idYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
rootYes
branchesYes
ancestorsYes
truncatedYes
next_cursorYes
completenessYes
more_branchesYes

TDQS

A4.3/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the readOnlyHint annotation, it discloses concrete behavioral constraints: two upstream reads, an upstream-controlled replies page, 100-comment and 4000-character caps, truncation semantics, and the meaning of more_branches/next_cursor. It also warns that missing parent links are not inferred.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three dense sentences with no filler. The most important facts (anonymous, public, no credentials) are front-loaded, and each sentence adds distinct operational value.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the simple read-only nature, annotations, and a present output schema, the description covers most of what an agent needs: scope, safety, limits, truncation, and incomplete-conversation signals. The only notable gap is input cursor usage, which is somewhat inferable from next_cursor.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

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 for the undocumented comment_id and cursor parameters. It mentions next_cursor in the output context but never explains the input cursor parameter or how comment_id identifies the thread.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with 'Anonymous public Note thread read', a specific verb plus resource, and immediately clarifies that no credentials are sent. This distinguishes it from authenticated or post-comment tools without needing to open the schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description clearly signals when this tool applies: anonymous public Note threads with no credentials. It does not explicitly name sibling alternatives or state when not to use it, so it stops short of a 5.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_postA
Read-only

Get the full content of a published post by ID. Returns title, body HTML, metadata.

ParametersJSON Schema
NameRequiredDescriptionDefault
post_idYesThe post ID to retrieve

Output Schema

ParametersJSON Schema
NameRequiredDescription
idYes
urlNo
slugNo
titleNo
audienceNo
subtitleNo
body_htmlNo
post_dateNo
word_countNo

TDQS

A4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The annotation indicates readOnlyHint, which covers the read-only nature. The description adds a constraint that the post must be published, but does not disclose other behavioral aspects such as error handling or side effects. Given the presence of the annotation, this is adequate but not exceptional.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise, consisting of a single sentence that states the action and the expected return data. It is well-structured and front-loaded, making it easy to parse.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple retrieval operation, the description is sufficiently complete: it names the input and output content. It does not address error cases or edge conditions, but given the simplicity and the presence of an output schema, this is not a significant gap.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema fully describes the only parameter (post_id) with a clear description. The tool description does not add any additional meaning to this parameter, so a baseline score of 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's function: retrieving the full content of a published post by ID, and explicitly mentions the returned data (title, body HTML, metadata). It distinguishes itself from sibling tools by specifying 'published post', making its purpose unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implicitly provides usage guidance by specifying that it operates on published posts, which contrasts with draft-related tools. However, it does not explicitly mention alternatives or when to prefer this tool over others, so it falls short of a 5.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_post_analyticsA
Read-only

Get performance stats (views, emails sent/delivered/opened, signups, subscribes, estimated value, comments, reactions) for a published post by ID. First reads the exact post detail (one authenticated read) and requires a published post with a post date. A draft, 403/404, malformed detail, ID mismatch, or other detail error except 401/429 triggers a scan of at most the 500 most recent published posts with up to 10 more reads. No writes. A feed-scan miss is bounded, not proof the post never existed; separate pages can shift. stats_available is false when a found post has no statistics. Per-post rates are upstream 0–1 fractions and are not added to this legacy projection.

ParametersJSON Schema
NameRequiredDescriptionDefault
post_idYesThe published post ID to get stats for

Output Schema

ParametersJSON Schema
NameRequiredDescription
idNo
noteNo
sentNo
foundYes
titleNo
viewsNo
openedNo
sourceNo
post_idNo
scannedNo
signupsNo
deliveredNo
post_dateNo
subscribesNo
feed_cappedNo
comment_countNo
search_resultNo
reaction_countNo
estimated_valueNo
stats_availableNo
detail_fallback_reasonNo

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description goes far beyond the readOnlyHint annotation by disclosing the one-read success path, the up-to-10-read scan fallback, the bounded semantics of a feed-scan miss, stats_available behavior, and the fact that per-post rates are upstream fractions. It also explicitly states 'No writes,' which is consistent with readOnlyHint=true.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the core action and immediately scannable metrics list. The trailing edge-case details—fallback scan limits, miss semantics, stats_available, and rate fractions—are dense but each adds necessary behavioral context. No filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With an output schema present, return values need not be spelled out. The description covers prerequisites, error-triggered fallback behavior, read limits, miss semantics, and special values like stats_available, making the tool's behavior fully predictable for an agent about to invoke it.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema already fully documents the single post_id parameter (100% coverage). The description reinforces that the ID must reference a published post and mentions ID mismatches, but it does not add new syntactic or format-level meaning beyond what the schema provides, so the baseline of 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

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: 'Get performance stats... for a published post by ID' and enumerates the exact metrics returned. It is unambiguous and implicitly distinguishes this per-post analytics tool from related siblings like get_publication_stats and get_post.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It clearly states the required precondition ('requires a published post with a post date') and explains the fallback scan behavior for drafts and errors. However, it does not explicitly say when to prefer this tool over sibling analytics or retrieval tools, 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.

get_post_commentsA
Read-only

Get comments on a published post. Returns commenter name, comment body, date, and reaction counts.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMax comments to return (default 20)
post_idYesThe post ID to get comments for

TDQS

A3.6/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The readOnlyHint annotation already covers the safety profile, and the description mentions returned data rather than side effects. However, it does not disclose behaviors such as pagination, ordering, or possible empty results, so transparency is adequate but not detailed.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact and information-dense, with exactly two sentences and no redundant phrasing. Every word contributes to understanding the tool's purpose or output.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple read operation, the description sufficiently identifies the required post_id and the returned fields. The absence of an output schema is partially mitigated by listing expected fields, though details like sorting or pagination behavior are not mentioned.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema fully documents both parameters, including constraints, defaults, and descriptions. The tool description adds no additional semantic nuance beyond what the schema already provides, so this dimension is at baseline.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Clearly states the action ('Get'), the resource ('comments on a published post'), and the returned fields. The qualifier 'published' helps distinguish from draft-related sibling tools, making the purpose immediately identifiable.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides no explicit guidance on when to use this tool versus alternatives, no named sibling tools, and no conditions or exclusions. The only contextual hint is 'published post', which is implicit rather than actionable guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_post_tagsA
Read-only

Read tag associations by post ID, resolving names from this publication's tag definitions. Includes hidden tags and preserves unresolved IDs. Returns 25 rows by default, at most 100, with local snapshot pagination. Each call makes up to three reads, including the full association and definition arrays; they are not an atomic snapshot. Empty associations do not verify post existence. Nonempty draft associations are not yet live-verified. Never assigns or removes tags.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNo
offsetNo
post_idYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
tagsYes
limitYes
totalYes
offsetYes
post_idYes
has_moreYes
returnedYes
paginationYes
next_offsetYes
publicationYes
post_identityYes
publication_idYes
pagination_noteYes
resolution_noteYes
resolution_scopeYes

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only provide readOnlyHint: true, so the description carries the behavioral disclosure burden. It goes far beyond that by revealing hidden tag inclusion, unresolved ID preservation, pagination limits, non-atomic multi-read behavior, and the caveat that empty or draft associations have verification limits. This is exemplary transparency.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense but efficient: every sentence adds a distinct piece of actionable information, and the core action is front-loaded. Caveats are packed without redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given an output schema exists to describe return values and annotations declare read-only safety, the description fills the remaining gaps: pagination semantics, transactional behavior, hidden tags, and edge-case verification limits. An agent has enough context to correctly invoke this tool and interpret unusual behavior.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

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. It adds meaning to limit and offset with 'Returns 25 rows by default, at most 100, with local snapshot pagination', and post_id is implied by 'by post ID'. It does not exhaustively bind every parameter, but it covers the non-obvious runtime semantics.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb-resource pair: 'Read tag associations by post ID', and clarifies it resolves names from tag definitions, includes hidden tags, and preserves unresolved IDs. This clearly differentiates it from siblings like list_publication_tags or get_post.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description makes the usage context clear: call this when you need tag associations for a specific post. It also states what the tool will not do ('Never assigns or removes tags'), which helps avoid using it for mutations, though it does not name alternative sibling tools explicitly.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_profile_feedA
Read-only

Anonymous public profile feed, no credentials sent. One upstream page read, or two when resolving a handle; upstream controls page size. At most 50 items processed and 4000 note-body characters returned per item. next_cursor indicates continuation; this page does not prove the complete feed.

ParametersJSON Schema
NameRequiredDescriptionDefault
cursorNo
handleNo
user_idNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
itemsYes
user_idYes
has_moreYes
returnedYes
next_cursorYes

TDQS

A3.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description adds substantial behavior beyond the readOnlyHint annotation: no credentials are sent, upstream page-read counts, upstream-controlled page size, item and character limits, and cursor semantics. These details give the agent an accurate mental model of side effects and pagination.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three compact sentences deliver high-value constraints without filler. The core qualifier 'anonymous public profile feed' is front-loaded, and each sentence adds a distinct behavioral fact.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description is strong on behavior, and the presence of an output schema reduces the need to document return values. However, it omits essential parameter-selection guidance for a tool with 0% schema-described parameters, leaving an agent uncertain how to form a valid request.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0% for 3 parameters, so the description must compensate by explaining cursor, handle, and user_id. It does not clarify whether these are alternatives, combinable, or which is required in practice; the only cursor mention is output-oriented ('next_cursor indicates continuation').

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly identifies the resource as an 'anonymous public profile feed' and the function as retrieving that feed. It adds useful qualifiers like 'no credentials sent' and processing limits, but it does not explicitly contrast siblings such as get_user_profile or list_public_posts.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives clear context: this tool is for anonymous, public, unauthenticated profile feed retrieval. It does not state exclusions or point to alternatives, so it leaves some routing to inference rather than fully selecting among siblings.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_publicationA
Read-only

Read projected identity and selected settings for this publication. Verifies the returned publication host; does not verify your account identity or admin role. Missing API fields are named explicitly. No changes are made.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
dataYes
publicationYes
identity_scopeYes
publication_urlYes
fields_not_returned_by_apiYes

TDQS

A4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The annotation indicates readOnlyHint, and the description reinforces this by stating 'No changes are made.' It additionally reveals verification behavior and how missing API fields are handled, providing useful behavioral context beyond the annotation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is brief and to the point, consisting of two sentences that cover the purpose, verification, and read-only nature without unnecessary detail.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the simple nature of the tool (no parameters, read-only), the description provides enough context about what it reads and how it behaves. It could mention the output format, but the description already notes that missing fields are named explicitly, which is helpful. Overall, it is complete for its complexity.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema has no parameters, so all parameters are documented (none). The description does not add parameter-specific information, but this is not needed. Baseline of 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's function: reading projected identity and selected settings for the publication. It uses a specific verb ('read') and identifies the resource ('publication'), distinguishing it from sibling tools that deal with posts, drafts, or notes.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description does not explicitly mention when to use this tool over alternatives, but it provides context about what it verifies (host) and what it does not (account identity, admin role). This gives some implicit guidance, but lacks explicit comparison to sibling tools.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_publication_statsA
Read-only

Read dashboard summary and summary-v2 for a trailing range of 1–365 days (default 30). Two authenticated reads, no writes. Each metric states its unit, window, source and missing state. Summary windows beyond named Last30Days fields are undocumented; summary values are not reconciled with summary-v2. A failed group is unavailable, never zero. ARR currency is not reported. Both groups unavailable with HTTP 403/404 means analytics access is unavailable.

ParametersJSON Schema
NameRequiredDescriptionDefault
range_daysNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
rangeYes
summaryYes
range_daysYes
publicationYes

TDQS

A4/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description far exceeds what readOnlyHint=true provides: it discloses two authenticated reads, per-metric unit/window/source/missing-state reporting, undocumented summary windows, non-reconciliation between summary and summary-v2, 'a failed group is unavailable, never zero' failure semantics, missing ARR currency, and the meaning of 403/404 on both groups. This is exactly the kind of behavioral context an agent cannot derive from annotations. It aligns with, rather than contradicts, the readOnlyHint.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Seven sentences, each carrying a distinct fact — purpose, auth mode, output format, two data caveats, failure semantics, currency gap, and error interpretation. No filler or restatement of schema fields, and the primary action is front-loaded in the first sentence.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The output schema covers return shape, and annotations cover safety, so the description's job was to cover quirks — which it does thoroughly. Minor gaps remain: the 'trailing' anchor date is unspecified, and there is no guidance on when to prefer summary over summary-v2. These are small enough that the tool is still callable correctly without them.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 0% schema description coverage, the description carries the full burden for range_days. It adds the 'trailing' temporal semantics and states the range/default, but the bounds and default are already present in the input schema, and the anchor point for 'trailing' (from the current date?) is left unspecified. The compensation is adequate for a single simple parameter but not rich.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

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 — 'Read dashboard summary and summary-v2 for a trailing range of 1–365 days (default 30)' — making the tool's scope precise. It doesn't explicitly name a sibling to differentiate from (e.g., get_post_analytics or get_growth_sources), but 'dashboard summary' clearly implies publication-level analytics rather than post-level, which is distinct enough for an agent to disambiguate.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage context is implied through 'dashboard summary' — the agent can infer this is for publication-wide stats rather than per-post analytics — but there is no explicit when-to-use guidance, no exclusions, and no named alternatives despite 32 siblings. The 403/404 note is diagnostic, not routing guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_public_postA
Read-only

Anonymous public post read by allowlisted /p/ URL, no credentials or subscription entitlements sent. One upstream read; body_html is capped at 500000 UTF-8 bytes. body_status is a heuristic from audience and body presence, not proof of full access or completeness.

ParametersJSON Schema
NameRequiredDescriptionDefault
urlYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
idYes
slugNo
titleNo
audienceNo
restacksNo
subtitleNo
body_htmlYes
post_dateNo
wordcountNo
body_statusYes
canonical_urlNo
comment_countNo
body_truncatedYes
reaction_countNo

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, so the description adds extra value: it discloses a single upstream read, a 500000-byte cap on body_html, and that body_status is a heuristic rather than proof of access or completeness. These details go well beyond the annotation and materially change how an agent interprets results. No contradiction with annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences, front-loaded with the core action and then constraints. Every clause adds essential information (auth-free, one read, size cap, heuristic status). No redundancy, and the structure makes the tool's scope immediately clear.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's simplicity (one parameter) and an existing output schema, the description covers all necessary context: what it reads, the URL type, the auth model, the response size limit, and the caveat on body_status. An agent has everything needed to call it correctly and interpret results aptly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0% (no description in schema), but the tool description states the URL must be an 'allowlisted /p/ URL', giving semantic meaning beyond type and maxLength. This partially compensates for the missing schema description. It would be a 5 with examples or format details, but a 4 is appropriate given the single parameter.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource: 'Anonymous public post read'. It further specifies the mechanism ('allowlisted /p/ URL') and clarifies that no credentials are used, which distinguishes it from authenticated tools like get_post or get_draft. The purpose is unambiguous and easily differentiated from siblings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description clearly indicates this tool is for anonymous reads of public posts via /p/ URLs, which implies it should be used when no authentication is available or desired. It does not explicitly name alternatives or exclusion conditions, but the context (anonymous, allowlisted) strongly guides selection. A bit more direct routing to siblings would lift this to a 5.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_sectionsA
Read-only

List your publication's sections (categories). Returns each section's id and name. Use a section id as section_id when creating or updating a draft to file it under that section.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The annotation already declares readOnlyHint=true, and the description adds that it returns id and name, which is consistent and sufficient. No contradictions.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two concise sentences: first states purpose and output, second gives usage guidance. No unnecessary words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple list tool with no parameters and no output schema, the description fully covers what the tool does, returns, and how to use it.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

No parameters exist, so schema coverage is 100%. The description does not need to add parameter explanations, meeting the baseline for 0 parameters.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the verb 'list' and the resource 'sections', and it distinguishes itself from sibling tools by focusing on listing categories.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It provides explicit guidance on using the section ID for drafts, which is the primary use case. However, it does not mention when not to use it, but that is acceptable for a simple list tool.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_subscriberA
Read-only

Look up a subscriber by exact email address. A listed free subscriber is a member even without paid access. Absence does not prove the address is eligible: Substack may suppress previous unsubscribes, and dashboard data can lag. Read-only; use to reconcile uncertain adds.

ParametersJSON Schema
NameRequiredDescriptionDefault
emailYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
noteYes
emailYes
last_syncYes
subscriberYes

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations declare readOnlyHint, and the description adds material behavioral caveats: a listed free subscriber counts as a member, absence may be due to suppression or lag, and absence does not prove ineligibility. These go well beyond the annotation and help the agent interpret results correctly.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Four sentences, each earning its place: core lookup, membership semantics, data caveats, and usage. The main action is front-loaded; only the word 'Read-only' is redundant with the annotation, which is negligible.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Complete for a one-parameter read-only lookup. Output schema covers return shape, and the description covers matching behavior, data reliability caveats, and the intended reconciliation use case. Nothing needed to call it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 0% schema description coverage, the description compensates by specifying 'exact email address,' which clarifies matching semantics beyond the schema's email format and maxLength. For a single self-describing parameter, this is sufficient.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific action ('Look up a subscriber') with an exact-match criterion on email address, which distinguishes it from sibling listing tools like list_subscribers and get_subscriber_count. The resource and lookup semantics are unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives a clear usage context: 'use to reconcile uncertain adds.' It does not explicitly name alternatives or state when not to use it, but the exact-email lookup and reconciliation purpose are enough for an agent to select it appropriately.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_subscriber_countA
Read-only

Get the current subscriber count for your Substack publication. Returns precision: 'exact' when the API reports a true count, 'approximate' when only Substack's rounded value is available (the real number is that or higher — render it hedged, e.g. '1,000+'), or 'unavailable' with count -1. Never treat an approximate value as exact.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
noteYes
countYes
precisionYes

TDQS

A4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The readOnlyHint annotation already indicates this is a read-only operation. The description adds valuable behavioral context by explaining the precision field (exact/approximate/unavailable) and the explicit warning 'Never treat an approximate value as exact.' This goes beyond the basic annotation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise and front-loaded with the primary purpose. It includes necessary detail about precision handling but is slightly verbose with the example and repeated caution. However, it remains focused and easy to parse.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description sufficiently covers the return value nuances, including all possible precision values and the meaning of count -1. While an output schema is indicated as present, the description alone provides enough context for typical usage. It does not mention error cases or edge conditions beyond the precision, but these are minor gaps.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters, and the input schema is an empty object. There are no parameter semantics to explain, so the description is fully sufficient. Schema coverage is 100% by definition.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action: 'Get the current subscriber count for your Substack publication.' It also explains the output precision semantics, making the purpose unambiguous. It distinguishes itself from sibling tools like list_subscribers or get_subscriber by focusing specifically on the aggregate count.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description does not provide any guidance on when to use this tool versus alternatives. It does not mention any conditions or contrasts with sibling tools like list_subscribers or get_subscriber, leaving the decision to the agent to infer.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_user_profileA
Read-only

Anonymous public profile read by handle; one upstream read, no credentials sent. Returns minimal public fields and the primary publication when marked. Public profile data does not prove account ownership or access.

ParametersJSON Schema
NameRequiredDescriptionDefault
handleYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
idYes
bioYes
nameYes
handleYes
photo_urlYes
primary_publicationYes

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Even with readOnlyHint already present, the description adds meaningful behavioral detail: one upstream read, no credentials sent, returns minimal public fields, and includes the primary publication only when marked. It also surfaces a semantic limitation that public data does not prove ownership/access.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three sentences, each earning its place: purpose and network/auth behavior, return scope, and an important caveat. The core purpose is front-loaded, and there is no repetition of schema or annotation fields.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

This is a low-complexity tool: one parameter, a readOnlyHint annotation, and an output schema already present. The description covers auth posture, network behavior, return scope, and a key semantic caveat, leaving nothing essential for correct invocation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, but the description embeds the sole parameter ('by handle') into its purpose statement, clarifying that handle is the lookup key. The schema's pattern covers format. For a single self-describing parameter, this is adequate compensation, though not deeply enriched.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb-resource pair: 'Anonymous public profile read by handle.' It clearly distinguishes this tool from siblings like get_publication or get_subscriber by emphasizing public, anonymous profile data, and the handle-based lookup is explicit.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description establishes clear context: use this for anonymous reads by handle with no credentials. It also warns that 'Public profile data does not prove account ownership or access,' which implicitly tells agents when not to rely on this tool. It does not explicitly name alternative sibling tools, so it stops short of full when-to-use routing.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_draftsB
Read-only

List draft posts. Returns title, creation date, and audience for each draft.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMax drafts to return (1-50; Substack rejects anything higher, so larger values are clamped)
offsetNoNumber of drafts to skip

TDQS

B3.4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, and the description adds useful context by specifying the returned fields, which matters since there is no output schema. However, it does not disclose behavioral details like ordering, whether draft content/body is omitted, or pagination effects beyond what the schema already covers.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two short sentences with no filler. The first sentence gives verb and resource, and the second states the output shape. Every word contributes information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple read-only list tool with fully documented optional parameters, this description is largely complete: it names the return fields even though no output schema exists. It could mention ordering or the lack of full draft content, but those are not essential for basic correct invocation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 100% description coverage: both limit and offset have meaningful descriptions. The tool description itself adds no parameter-level semantics, so the baseline of 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource ('List draft posts') and enumerates the returned fields (title, creation date, audience). It is distinguishable from list_scheduled_posts by the word 'drafts', but it does not explicitly contrast itself with get_draft or export_draft.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no guidance on when to use list_drafts versus siblings like list_scheduled_posts, get_draft, or export_draft. The description only says what the tool does, with no conditions, exclusions, or alternative routing.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_publication_tagsA
Read-only

Read this publication's tag definitions. Includes hidden tags by default. Returns 25 rows by default, at most 100. Each call makes two reads (publication context and the full tag array), then paginates locally; results can change between calls. Validates publication identity and rejects malformed or oversized responses. Never creates or assigns tags.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNo
offsetNo
include_hiddenNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
tagsYes
limitYes
totalYes
offsetYes
has_moreYes
returnedYes
paginationYes
next_offsetYes
publicationYes
include_hiddenYes
publication_idYes
pagination_noteYes

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description goes well beyond the readOnlyHint annotation by disclosing default row counts, maximum rows, local pagination behavior, the two reads per call, potential inconsistency between calls, validation behavior, and the explicit guarantee that it never creates or assigns tags. This is exemplary transparency.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact and front-loaded with the core purpose, then adds behavioral details in a logical order. Every sentence carries useful information with no filler or redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the output schema exists and the annotations provide the read-only guarantee, the description covers defaults, limits, pagination semantics, consistency caveats, validation, and side-effect absence. Nothing critical is missing for an agent to invoke this tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The description explains default row counts and maximum rows, which map to limit, and mentions hidden tags by default, which maps to include_hidden. However, offset is not explicitly described; the mention of local pagination and changing results implies its behavior but does not fully compensate for the 0% schema description coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

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: 'Read this publication's tag definitions.' It clearly identifies the operation and scope, and the detail about including hidden tags by default differentiates it from related tag tools like get_post_tags.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The phrase 'this publication's tag definitions' gives clear context for when to use the tool, and the sibling list confirms there is a distinct get_post_tags tool for post-level tags. It does not explicitly name alternatives or exclusions, but the resource distinction is clear enough.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_public_postsA
Read-only

Anonymous public archive read, no credentials sent. One upstream read of 1–50 posts (default 12); sort and search are upstream controlled. A full page gives next_offset, but has_more is unknown because Substack returns no total. Public metadata does not prove access to post bodies.

ParametersJSON Schema
NameRequiredDescriptionDefault
sortNonew
limitNo
queryNo
offsetNo
publication_urlNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
sortYes
limitYes
postsYes
queryYes
offsetYes
has_moreYes
returnedYes
next_offsetYes
publication_urlYes

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the readOnlyHint, the description discloses that no credentials are sent, exactly one upstream read occurs, pagination provides next_offset but not reliable has_more, and public metadata does not guarantee body access. This is rich behavioral context with no contradiction.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Four short sentences, densely packed with relevant constraints and caveats, with the central purpose front-loaded. There is no filler or repetition of schema details.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With an output schema present, return-value details need no description. The description covers access mode, pagination behavior, and data-access limitations. The only minor gap is that publication_url is not explicitly identified as the target archive selector, though the parameter name makes it recoverable.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0%, but the description compensates for most parameters: limit bounds and default, sort and query behavior, and offset via next_offset are all addressed. publication_url is left to the schema name, but its meaning is clear enough.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening phrase 'Anonymous public archive read' states a specific verb, resource, and key differentiator (no credentials), clearly identifying the list operation. The count range and public scope further distinguish it from auth-required sibling tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Clear context is provided: this is for reading public archives anonymously, so an agent can infer not to use it for subscriber-specific or draft operations. No explicit alternative or when-not condition is named, so it stops short of a 5.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_published_postsA
Read-only

List published posts with pagination. Returns title, date, slug, and URL for each post.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMax posts to return (1-50; Substack rejects anything higher, so larger values are clamped)
offsetNoNumber of posts to skip

Output Schema

ParametersJSON Schema
NameRequiredDescription
postsYes
totalNo

TDQS

A4.4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The readOnlyHint annotation already covers safety; the description adds output details but does not go beyond that, so limited additional transparency.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two concise sentences, no fluff, and the key purpose is front-loaded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With an output schema present and a clear mention of return fields, the description is complete for the intended use.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Both parameters (limit and offset) have clear, detailed descriptions including clamping behavior, with full schema coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Clearly states the action ('List published posts') and resource, distinguishing it from similar tools like list_scheduled_posts.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides clear context on pagination and return fields, but does not explicitly mention alternatives or when to prefer this over search_posts.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_scheduled_postsA
Read-only

List posts scheduled for future publication, soonest first. Read-only visibility into what's queued — scheduling itself is done in Substack's editor (this server does not schedule, publish, or delete long-form posts). Returns id, title, audience, and scheduled time (trigger_at).

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMax posts to return (1-50; Substack rejects anything higher, so larger values are clamped)
offsetNoNumber of posts to skip

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The readOnlyHint annotation is true and the description reinforces it by stating the tool does not schedule, publish, or delete posts. This fully discloses the tool's side effects (none).

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise and front-loaded with purpose and read-only nature. It avoids fluff and clearly states the return fields, making every sentence valuable.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Even without an output schema, the description lists what is returned (id, title, audience, scheduled time). Combined with the read-only note and parameter descriptions, an agent has enough context to invoke the tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema provides complete descriptions for both parameters (limit and offset), so the baseline is 3. The description adds no extra parameter-specific information beyond the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Clearly states the verb 'List' and resource 'posts scheduled for publication', with the ordering 'soonest first'. The read-only note and reference to scheduling in the editor distinguish it from sibling tools like list_published_posts and list_drafts.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly says 'Read-only visibility into what's queued' and that scheduling itself is done in Substack's editor, clarifying when not to use this tool. It also mentions no publishing or deletion, providing clear boundaries.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_subscribersA
Read-only

Read a page of private subscriber email addresses and subscription IDs. Dashboard data may lag recent changes. Use get_subscriber for exact membership checks.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNo
offsetNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
countYes
lastSyncNo
subscribersYes

TDQS

A4.6/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The readOnlyHint annotation is reinforced by the non-destructive 'Read' wording. The description adds useful behavioral context about privacy and potential lag without contradicting the annotation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two short, focused sentences with no redundant wording. Every clause adds value: the action, the data privacy, the lag caveat, and the alternative tool.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Output schema exists, so output fields need no description. The privacy warning, lag caveat, and alternative-tool pointer give sufficient operational context for a straightforward paginated read operation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

limit and offset are not explicitly described, but the phrase 'Read a page' combined with the schema's defaults and bounds makes their pagination role clear enough. Slight extra explanation would be needed for full clarity, but the intent is unambiguous.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Description clearly states the operation ('Read a page'), the resource ('subscriber email addresses and subscription IDs'), and the privacy scope. It also distinguishes this from get_subscriber by directing exact membership checks there.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly tells agents when not to use this tool ('Use get_subscriber for exact membership checks') and warns about data lag, giving practical context for approximate/bulk membership listing.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

plan_draft_updateA
Read-only

Read an unpublished draft and review proposed Markdown/metadata changes, bounded previews, conversion losses and preflight. Returns a receipt binding the observed state and exact payload for update_draft. No writes. Hashes check consistency, not human approval; stale detection is best-effort, not atomic.

ParametersJSON Schema
NameRequiredDescriptionDefault
bodyNo
titleNo
audienceNo
draft_idYes
subtitleNo
allow_unsupportedNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
changesYes
receiptYes
preflightYes
editor_urlYes
limitationsYes
changed_fieldsYes
format_versionYes
scheduling_policyYes
unsupported_nodesYes
proposed_markdown_previewYes
markdown_preview_truncatedYes
scheduling_fields_not_returnedYes

TDQS

A3.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description goes well beyond the readOnlyHint annotation by adding 'No writes,' explaining the receipt binding observed state, and disclosing limitations: 'Hashes check consistency, not human approval; stale detection is best-effort, not atomic.' This is strong behavioral context that helps an agent trust and interpret the result.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact and front-loaded with the core purpose in the first sentence. Each sentence adds value, though the list 'bounded previews, conversion losses and preflight' is jargon-heavy and slightly awkward.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The safety profile, no-write guarantee, receipt semantics, and consistency caveats are well covered, and an output schema exists to describe return values. However, with 6 parameters and zero schema descriptions, the lack of parameter guidance is a real gap, and the relationship to preflight_draft is left implicit.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description carries the burden of explaining parameters, but it never mentions draft_id, allow_unsupported, audience, or subtitle. 'Markdown/metadata changes' loosely hints at body/title/subtitle but doesn't clarify the important flags or defaults.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it reads/reviews an unpublished draft and proposed Markdown/metadata changes, and it distinguishes itself from update_draft by explicitly saying 'No writes' and returning a receipt for update_draft. However, the word 'preflight' overlaps with the sibling preflight_draft, so it doesn't fully separate itself from that tool.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The phrase 'exact payload for update_draft' plus 'No writes' establishes clear context that this is a planning/preview step before updating a draft. It doesn't provide explicit when-not guidance or name alternatives such as preflight_draft or get_draft.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

preflight_draftA
Read-only

Read a draft and check title, audience, body structure, images and paywalls. Static review aid only: never modifies or publishes; does not guarantee rendering, link availability or publish readiness. Review the findings in Substack.

ParametersJSON Schema
NameRequiredDescriptionDefault
draft_idYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
countsYes
draft_idYes
findingsYes
editor_urlYes
limitationsYes
publicationYes
checks_passedYes

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description fully discloses that the tool is read-only and non-destructive, aligning with the readOnlyHint annotation and adding concrete details about what it does not guarantee.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise, well-structured in three sentences, and covers the essential aspects without unnecessary verbosity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

It provides sufficient context about what the tool does, its limitations, and where to review results (Substack), making it complete given the output schema exists.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The only parameter, draft_id, is not described in the schema (0% coverage) and the description does not explicitly explain it beyond the tool's context, so it only partially compensates for the missing schema documentation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool reads a draft and checks specific aspects (title, audience, body structure, images, paywalls), making its purpose unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explicitly notes it is a static review aid that never modifies or publishes, and it clarifies limitations (no rendering/link/publish guarantees), guiding appropriate usage.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

rank_postsA
Read-only

Rank posts by one metric from Substack's dashboard email statistics: views, opened, sent, open_rate, click_through_rate, signups, subscribes, estimated_value or post_date, descending or ascending. Returns 10 rows by default, at most 20 (Substack's page limit), with total and next_offset for continuation. One read; nothing is changed. Values are passed through as Substack reports them: this server does not recompute, fill in or estimate metrics, and Substack does not document rate denominators. Each row marks the ranked value as reported, null or absent; null and absent are not zero, and null rates can appear among numeric rows. For one post's stats by ID, use get_post_analytics.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNo
metricNoviews
offsetNo
directionNodesc

Output Schema

ParametersJSON Schema
NameRequiredDescription
rowsYes
limitYes
totalYes
metricYes
offsetYes
sourceYes
has_moreYes
orderingYes
returnedYes
directionYes
semanticsYes
next_offsetYes
publicationYes
unreported_in_pageYes

TDQS

A5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the readOnlyHint annotation, the description adds critical behavior: it returns default/max rows, supports pagination via total and next_offset, passes values through unmodified, does not recompute metrics, and explains that null/absent are distinct from zero. These details prevent an agent from assuming the tool cleans or infers data.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is information-dense and front-loaded with the core purposeable immediately actionable. Every sentence contributes substantive value—purpose, pagination, read safety, data fidelity, null semantics, and sibling routing. The only minor redundancy is "One read; nothing is changed," which is short and reinforces rather than bloats.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the output schema exists for return valuesстоит, the description covers everything needed to call the tool correctly: parameter meanings, defaults, limits, pagination, behavioral caveats, and alternative tool routing. It is complete enough for agent invocation without further inference.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 0% schema description coverage, the description carries the full burden. It enumerates every metric enum value, explains the limit default and maximum, conveys offset continuation semantics through next_offset, and clarifies sort direction. It also provides important caveats about rate denominators and null handling that the schema alone could not convey.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

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: "Rank posts by one metric from Substack's dashboard email statistics." It enumerates the exact metrics and sort directions, making the tool's purpose unambiguous. The final sentence explicitly distinguishes it from get_post_analytics, which is the closely related sibling for single-post stats.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description clearly states the intended use case—ranking multiple posts by a dashboard metric—and explicitly routes single-post stats to get_post_analytics. This gives the agent a direct decision rule for choosing between the two most similar tools.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

search_postsA
Read-only

Search this publication's published, draft, or scheduled archive using Substack's server-side query. One page per call, at most 50 results; use next_offset to continue. Matching/indexing is controlled by Substack, not a guaranteed full-text scan. Returns metadata only; get_post/get_draft fetch full content.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNo
queryYes
offsetNo
statusNopublished

Output Schema

ParametersJSON Schema
NameRequiredDescription
limitYes
postsYes
queryYes
totalYes
offsetYes
statusYes
has_moreYes
returnedYes
next_offsetYes
publicationYes
search_scopeYes

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the readOnlyHint annotation, the description discloses pagination behavior ('One page per call, at most 50 results'), continuation via 'next_offset', server-controlled indexing limitations, and metadata-only return values. These details set accurate expectations about how the search behaves and what it cannot guarantee.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is three tight sentences with no filler. The main scoping is front-loaded, followed by pagination caveats and the metadata-only/content-fetching distinction.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the presence of an output schema and readOnly annotations, the description covers everything needed to call the tool correctly: scope, statuses, pagination limits, continuation mechanism, and search-quality caveats. The minor offset/next_offset wording ambiguity is resolvable from the input schema.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 0% schema description coverage, the description compensates by explaining page size, pagination continuation, and the published/draft/scheduled statuses. However, it references 'next_offset' without explicitly mapping it to the schema's 'offset' parameter, and it leaves limit defaults to the schema's own constraints.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description names a specific action ('Search'), a precise resource ('this publication's published, draft, or scheduled archive'), and the mechanism ('Substack's server-side query'). It also explicitly distinguishes itself from content-fetching tools by stating 'Returns metadata only; get_post/get_draft fetch full content.'

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description routes full-content needs to get_post/get_draft and clarifies that search is not a guaranteed full-text scan. It does not explicitly contrast search_posts with the list_* siblings, so an agent must infer when to choose query-based search over simple enumeration.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

search_subscribersA
Read-only

Read one page of private subscriber data with Substack-side filters and sorting. One authenticated read, no writes; 1–50 rows (default 10). Returns email, subscription ID and interval by default; include selects extra fields. total_matching is Substack's count at read time; dashboard data may lag writes and pagination is not a snapshot. Search matching is controlled by Substack, and a result does not prove all current subscribers were captured.

ParametersJSON Schema
NameRequiredDescriptionDefault
sortNocreated_desc
limitNo
offsetNo
searchNo
includeNo
created_beforeNoYYYY-MM-DD, exclusive: created before the start of this date; Substack's day boundary timezone is not verified
subscription_typesNo
activity_rating_maxNo
activity_rating_minNo
created_on_or_afterNoYYYY-MM-DD, created on or after this date

Output Schema

ParametersJSON Schema
NameRequiredDescription
noteYes
sortYes
limitYes
offsetYes
has_moreYes
returnedYes
next_offsetYes
subscribersYes
total_matchingYes
applied_filtersYes

TDQS

A3.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the readOnlyHint annotation, it discloses row limits (1–50, default 10), default return fields, the include parameter behavior, total_matching caveats, pagination not being a snapshot, and that search matching is controlled by Substack. This is rich, non-obvious behavioral detail that helps an agent understand reliability and limitations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single paragraph with front-loaded purpose, followed by key constraints and caveats. It is information-dense but not bloated; every sentence contributes meaning. Could be slightly more structured but is appropriately concise.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 10-parameter tool with low schema coverage, the description covers critical behavioral aspects (return fields, pagination, search semantics) but leaves several parameters unexplained. An agent might still struggle to construct a correct request without additional parameter insight, though the tool name and schema provide some clues.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is only 20% (two date params have descriptions). The description mentions 'include selects extra fields' and implies filters/sorting, but it does not explain the meaning or usage of most parameters (sort, limit, offset, search, subscription_types, activity_rating_min/max, etc.). It fails to compensate for the low schema coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb ('read') and resource ('private subscriber data') with 'Substack-side filters and sorting'. This clearly distinguishes it from public post tools and establishes it as a read operation on subscriber data. Though it doesn't name siblings, the purpose is unmistakable.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides context about server-side filtering and sorting, which implies when it should be used, but it does not explicitly state when to prefer this over alternatives like list_subscribers or get_subscriber. No exclusions or alternative routing are given.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

update_draftA
Destructive

Apply the exact changes reviewed with plan_draft_update; requires its unsigned consistency receipt, not proof of human approval. Rechecks publication, unpublished state and fingerprint before one PUT, then reads back. Rejects known stale or changed payloads. A read/write race remains. Inspect unverified/conflict outcomes in Substack; never automatically retry. Accepts Markdown; does not publish or schedule.

ParametersJSON Schema
NameRequiredDescriptionDefault
bodyNo
titleNo
receiptYes
audienceNo
draft_idYes
subtitleNo
allow_unsupportedNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
codeYes
statusYes
messageYes
draft_idYes
editor_urlYes
limitationsYes
publicationYes
changed_fieldsYes
format_versionYes
publication_idYes
request_statusYes
write_attemptsYes
mismatched_fieldsYes
unsupported_nodesYes

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the annotations' destructive/read-only flags, it discloses the pre-PUT recheck, single PUT plus read-back, stale/changed-payload rejection, the remaining read/write race, and the need to inspect conflict outcomes manually. No contradiction with annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Every sentence carries distinct operational information: precondition, verification, mutation, race, retry policy, format, and non-side-effects. No filler or redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a destructive, race-prone update tool, the description covers preconditions, safety checks, failure behavior, retry prohibition, and side-effect boundaries. Output schema handles the return shape, so nothing needed for correct invocation is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 0% schema description coverage, the text explains the crucial receipt contract (unsigned consistency receipt, not approval), ties all editable fields to 'exact changes reviewed' in the plan, and notes Markdown support. It leaves audience and allow_unsupported implicit, but the plan-dependency makes their meaning recoverable.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description names a precise action ('apply exact changes reviewed with plan_draft_update') and resource (draft), and explicitly excludes publishing/scheduling. This makes it immediately distinguishable from create_draft, plan_draft_update, and scheduling siblings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It states the required precondition (receipt from plan_draft_update), clarifies that the receipt is not human approval, and tells the agent never to auto-retry. This is concrete when-to-use and when-not-to-act guidance tied to a named sibling.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

update_draft_tagsA
Destructive

Assign or remove up to 20 distinct tag IDs per direction on a draft; refuses published or scheduled drafts before writing; not atomic — see draft_state_after. Dry-run defaults to true. Reads publication context, definitions, draft and associations (four reads); a live change rechecks the draft before writing, then reads draft state and associations after writing (up to seven reads total). Sends at most 40 sequential writes, each once, with no automatic retry. Only a confirmed request observed in readback while the draft remains unpublished is verified. Hidden tags are allowed and reported. Draft tags may become public when you later publish the draft in Substack.

ParametersJSON Schema
NameRequiredDescriptionDefault
addNo
removeNo
dry_runNo
draft_idYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
noteYes
dry_runYes
resultsYes
draft_idYes
publicationYes
write_attemptsYes
draft_state_afterYes

TDQS

A4.4/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description goes well beyond the annotations (destructiveHint=true, readOnlyHint=false) by disclosing non-atomicity, a dry-run default of true, a precise read/write budget (four reads, up to seven reads total, at most 40 sequential writes with no retry), readback-based verification semantics, and the fact that hidden tags are allowed and may become public on publish. Each of these is an operational trait an agent cannot infer from the schema or annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Purpose is front-loaded in the first sentence, followed by cost- and safety-relevant behavior, and no sentence is fluff. The middle sentences on read counts and readback verification are dense and technical, arguably more operational detail than an agent needs purely for correct selection, so it is appropriately sized but slightly over-specified.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a mutating, non-idempotent tool whose output schema is available (covering return values), the description covers everything needed to call it correctly: accepted inputs, refusal conditions, dry-run semantics, failure/retry behavior, verification requirements, and post-publish consequences. No critical gap remains.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 0% schema description coverage, the description carries the full parameter burden, and it mostly delivers: 'tag IDs' clarifies the add/remove arrays, 'up to 20 distinct ... per direction' adds a uniqueness constraint absent from the schema, and 'Dry-run defaults to true' explains dry_run's behavior. It does not offer concrete examples or edge-case handling (e.g., duplicate-add behavior), leaving a little to inference.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening clause 'Assign or remove up to 20 distinct tag IDs per direction on a draft' names a specific verb (assign/remove), resource (tag IDs on a draft), and a scope limit (20 per direction). This clearly distinguishes it from read-only siblings like get_post_tags and list_publication_tags, as well as the general-purpose update_draft.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description states a hard refusal for 'published or scheduled drafts,' which implicitly defines when the tool is inapplicable, and notes that dry-run defaults to true, implying safe preview usage. However, it never names alternative tools (e.g., update_draft for non-tag draft edits, preflight_draft for preflight checks) or gives explicit when-to-use routing, so the guidance is implied rather than directive.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

upload_imageA

Upload an image to Substack's CDN. Provide exactly one of image_base64 (a base64 data URI), image_path (a local file path) or image_url (a public HTTPS image to download first). Returns a hosted image URL that is publicly fetchable by anyone with the link (an unlisted asset — not attributed to you or added to your feed).

ParametersJSON Schema
NameRequiredDescriptionDefault
image_urlNoHTTPS URL of a PNG, JPEG, GIF, WebP or AVIF image to download and upload. Sent without Substack cookies; private, loopback, link-local, metadata and reserved destinations are refused at connection time and on every redirect (at most 3). Limits: 5 MB and 15 seconds; the bytes must match the declared type. Not available on every deployment. Mutually exclusive with image_base64 and image_path.
image_pathNoAbsolute path to a local image file (e.g., "/Users/me/pic.png"). Read and encoded automatically; MIME type inferred from the extension. Mutually exclusive with image_base64 and image_url.
image_base64NoBase64-encoded image with data URI prefix (e.g., "data:image/png;base64,..."). Mutually exclusive with image_path and image_url.

Output Schema

ParametersJSON Schema
NameRequiredDescription
image_urlYes

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only set flags (readOnlyHint=false, destructiveHint=false, openWorldHint=true). The description adds substantial behavioral context: the hosted URL is 'publicly fetchable by anyone with the link' and 'an unlisted asset — not attributed to you or added to your feed'. It also discloses important security details for `image_url` (sent without cookies, refusal of private/loopback destinations, redirect limit of 3). This goes well beyond what annotations provide.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact but information-dense. The main action and the 'exactly one' rule appear in the first sentence, followed by necessary clarifications in parentheses. No fluff, but the parenthetical about `image_url` security is a slight digression; still, it's essential context. Overall well-structured.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The tool has three mutually exclusive parameters, security constraints, and an output schema (which describes the return value). The description covers the input requirements, behavior, and return (hosted URL) sufficiently. There is no missing information that would prevent an agent from invoking it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema already describes each parameter at 100% coverage. The description reinforces mutual exclusivity and adds practical constraints for `image_url` (5 MB, 15 seconds, type matching) and for `image_path` (MIME inferred from extension). This adds value beyond the schema without redundancy.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Description states a specific verb ('Upload') and resource ('image to Substack's CDN'), which is clear and distinct from all sibling tools (export, get, list, create, etc.). No ambiguity about what this tool accomplishes.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly mandates 'Provide exactly one of' the three input modes, which is a clear usage rule. It also notes that `image_url` is 'Not available on every deployment' and includes mutual-exclusivity language. However, it does not explicitly state when to prefer this tool over alternatives, though no sibling upload tool exists, so that gap is minor.

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.

  1. 10 tool updatesv1.3.0
    • Addedget_growth_sources
    • Addedget_note_thread
    • Changedget_post_analytics2 fields changed
      • addedOutput schema / properties / detail_fallback_reason
        Added value: +{
        +  "enum": [
        +    "not_found",
        +    "malformed",
        +    "id_mismatch",
        +    "forbidden",
        +    "not_published",
        +    "upstream_error"
        +  ],
        +  "type": "string"
        +}
      • addedOutput schema / properties / source
        Added value: +{
        +  "enum": [
        +    "post_detail",
        +    "published_feed_scan"
        +  ],
        +  "type": "string"
        +}
    • Addedget_profile_feed
    • Addedget_public_post
    • Addedget_publication_stats
    • Addedget_user_profile
    • Addedlist_public_posts
    • Addedsearch_subscribers
    • Addedupdate_draft_tags
  2. 3 tool updatesv1.2.0
    • Changedget_post_analytics4 fields changed
      • addedOutput schema / properties / feed_capped
        Added value: +{
        +  "type": [
        +    "boolean",
        +    "null"
        +  ]
        +}
      • addedOutput schema / properties / scanned
        Added value: +{
        +  "maximum": 9007199254740991,
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • addedOutput schema / properties / search_result
        Added value: +{
        +  "enum": [
        +    "archive_exhausted",
        +    "scan_bound_reached",
        +    "feed_incomplete"
        +  ],
        +  "type": "string"
        +}
      • addedOutput schema / properties / stats_available
        Added value: +{
        +  "type": "boolean"
        +}
    • Addedrank_posts
    • Changedupload_image3 fields changed
      • changedInput schema / properties / image_base64 / description
        Previous value: -"Base64-encoded image with data URI prefix (e.g., \"data:image/png;base64,...\"). Mutually exclusive with image_path."New value: +"Base64-encoded image with data URI prefix (e.g., \"data:image/png;base64,...\"). Mutually exclusive with image_path and image_url."
      • changedInput schema / properties / image_path / description
        Previous value: -"Absolute path to a local image file (e.g., \"/Users/me/pic.png\"). Read and encoded automatically; MIME type inferred from the extension. Mutually exclusive with image_base64."New value: +"Absolute path to a local image file (e.g., \"/Users/me/pic.png\"). Read and encoded automatically; MIME type inferred from the extension. Mutually exclusive with image_base64 and image_url."
      • addedInput schema / properties / image_url
        Added value: +{
        +  "description": "HTTPS URL of a PNG, JPEG, GIF, WebP or AVIF image to download and upload. Sent without Substack cookies; private, loopback, link-local, metadata and reserved destinations are refused at connection time and on every redirect (at most 3). Limits: 5 MB and 15 seconds; the bytes must match the declared type. Not available on every deployment. Mutually exclusive with image_base64 and image_path.",
        +  "maxLength": 2048,
        +  "type": "string"
        +}
  3. 23 tool updates
    • Addedadd_free_subscriber
    • Changedcreate_draft2 fields changed
      • addedInput schema / properties / allow_unsupported
        Added value: +{
        +  "default": false,
        +  "description": "Acknowledge conversion diagnostics and retain unsupported Markdown literally in this private draft",
        +  "type": "boolean"
        +}
      • changedOutput schema / (root)
        Previous value: -nullNew value: +{
        +  "$schema": "http://json-schema.org/draft-07/schema#",
        +  "additionalProperties": false,
        +  "properties": {
        +    "id": {
        +      "exclusiveMinimum": 0,
        +      "maximum": 9007199254740991,
        +      "type": "integer"
        +    },
        +    "message": {
        +      "type": "string"
        +    },
        +    "title": {
        +      "type": [
        +        "string",
        +        "null"
        +      ]
        +    },
        +    "unsupported_nodes": {
        +      "items": {
        +        "additionalProperties": true,
        +        "properties": {},
        +        "type": "object"
        +      },
        +      "type": "array"
        +    }
        +  },
        +  "required": [
        +    "id",
        +    "unsupported_nodes",
        +    "message"
        +  ],
        +  "type": "object"
        +}
    • Changedcreate_note1 field changed
      • changedOutput schema / (root)
        Previous value: -nullNew value: +{
        +  "$schema": "http://json-schema.org/draft-07/schema#",
        +  "additionalProperties": false,
        +  "properties": {
        +    "attachment_id": {
        +      "$ref": "#/properties/message"
        +    },
        +    "body": {
        +      "type": [
        +        "string",
        +        "null"
        +      ]
        +    },
        +    "date": {
        +      "$ref": "#/properties/body"
        +    },
        +    "id": {
        +      "exclusiveMinimum": 0,
        +      "maximum": 9007199254740991,
        +      "type": "integer"
        +    },
        +    "message": {
        +      "type": "string"
        +    }
        +  },
        +  "required": [
        +    "id",
        +    "message"
        +  ],
        +  "type": "object"
        +}
    • Changedcreate_note_with_link1 field changed
      • changedOutput schema / (root)
        Previous value: -nullNew value: +{
        +  "$schema": "http://json-schema.org/draft-07/schema#",
        +  "additionalProperties": false,
        +  "properties": {
        +    "attachment_id": {
        +      "$ref": "#/properties/message"
        +    },
        +    "body": {
        +      "type": [
        +        "string",
        +        "null"
        +      ]
        +    },
        +    "date": {
        +      "$ref": "#/properties/body"
        +    },
        +    "id": {
        +      "exclusiveMinimum": 0,
        +      "maximum": 9007199254740991,
        +      "type": "integer"
        +    },
        +    "message": {
        +      "type": "string"
        +    }
        +  },
        +  "required": [
        +    "id",
        +    "message",
        +    "attachment_id"
        +  ],
        +  "type": "object"
        +}
    • Addedexport_draft
    • Changedget_draft4 fields changed
      • addedInput schema / properties / draft_id / exclusiveMinimum
        Added value: +0
      • addedInput schema / properties / draft_id / maximum
        Added value: +9007199254740991
      • changedInput schema / properties / draft_id / type
        Previous value: -"number"New value: +"integer"
      • changedOutput schema / (root)
        Previous value: -nullNew value: +{
        +  "$schema": "http://json-schema.org/draft-07/schema#",
        +  "additionalProperties": false,
        +  "properties": {
        +    "audience": {
        +      "$ref": "#/properties/title"
        +    },
        +    "body": {
        +      "$ref": "#/properties/title"
        +    },
        +    "created_at": {
        +      "$ref": "#/properties/title"
        +    },
        +    "id": {
        +      "exclusiveMinimum": 0,
        +      "maximum": 9007199254740991,
        +      "type": "integer"
        +    },
        +    "subtitle": {
        +      "$ref": "#/properties/title"
        +    },
        +    "title": {
        +      "type": [
        +        "string",
        +        "null"
        +      ]
        +    },
        +    "updated_at": {
        +      "$ref": "#/properties/title"
        +    },
        +    "word_count": {
        +      "anyOf": [
        +        {
        +          "maximum": 9007199254740991,
        +          "minimum": 0,
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ]
        +    }
        +  },
        +  "required": [
        +    "id"
        +  ],
        +  "type": "object"
        +}
    • Changedget_post4 fields changed
      • addedInput schema / properties / post_id / exclusiveMinimum
        Added value: +0
      • addedInput schema / properties / post_id / maximum
        Added value: +9007199254740991
      • changedInput schema / properties / post_id / type
        Previous value: -"number"New value: +"integer"
      • changedOutput schema / (root)
        Previous value: -nullNew value: +{
        +  "$schema": "http://json-schema.org/draft-07/schema#",
        +  "additionalProperties": false,
        +  "properties": {
        +    "audience": {
        +      "$ref": "#/properties/title"
        +    },
        +    "body_html": {
        +      "$ref": "#/properties/title"
        +    },
        +    "id": {
        +      "exclusiveMinimum": 0,
        +      "maximum": 9007199254740991,
        +      "type": "integer"
        +    },
        +    "post_date": {
        +      "$ref": "#/properties/title"
        +    },
        +    "slug": {
        +      "$ref": "#/properties/title"
        +    },
        +    "subtitle": {
        +      "$ref": "#/properties/title"
        +    },
        +    "title": {
        +      "type": [
        +        "string",
        +        "null"
        +      ]
        +    },
        +    "url": {
        +      "$ref": "#/properties/title"
        +    },
        +    "word_count": {
        +      "anyOf": [
        +        {
        +          "maximum": 9007199254740991,
        +          "minimum": 0,
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ]
        +    }
        +  },
        +  "required": [
        +    "id"
        +  ],
        +  "type": "object"
        +}
    • Changedget_post_analytics4 fields changed
      • addedInput schema / properties / post_id / exclusiveMinimum
        Added value: +0
      • addedInput schema / properties / post_id / maximum
        Added value: +9007199254740991
      • changedInput schema / properties / post_id / type
        Previous value: -"number"New value: +"integer"
      • changedOutput schema / (root)
        Previous value: -nullNew value: +{
        +  "$schema": "http://json-schema.org/draft-07/schema#",
        +  "additionalProperties": false,
        +  "properties": {
        +    "comment_count": {
        +      "$ref": "#/properties/views"
        +    },
        +    "delivered": {
        +      "$ref": "#/properties/views"
        +    },
        +    "estimated_value": {
        +      "anyOf": [
        +        {
        +          "type": "number"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ]
        +    },
        +    "found": {
        +      "type": "boolean"
        +    },
        +    "id": {
        +      "$ref": "#/properties/post_id"
        +    },
        +    "note": {
        +      "type": "string"
        +    },
        +    "opened": {
        +      "$ref": "#/properties/views"
        +    },
        +    "post_date": {
        +      "$ref": "#/properties/title"
        +    },
        +    "post_id": {
        +      "exclusiveMinimum": 0,
        +      "maximum": 9007199254740991,
        +      "type": "integer"
        +    },
        +    "reaction_count": {
        +      "$ref": "#/properties/views"
        +    },
        +    "sent": {
        +      "$ref": "#/properties/views"
        +    },
        +    "signups": {
        +      "$ref": "#/properties/views"
        +    },
        +    "subscribes": {
        +      "$ref": "#/properties/views"
        +    },
        +    "title": {
        +      "type": [
        +        "string",
        +        "null"
        +      ]
        +    },
        +    "views": {
        +      "anyOf": [
        +        {
        +          "maximum": 9007199254740991,
        +          "minimum": 0,
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ]
        +    }
        +  },
        +  "required": [
        +    "found"
        +  ],
        +  "type": "object"
        +}
    • Changedget_post_comments6 fields changed
      • addedInput schema / properties / limit / maximum
        Added value: +100
      • addedInput schema / properties / limit / minimum
        Added value: +1
      • changedInput schema / properties / limit / type
        Previous value: -"number"New value: +"integer"
      • addedInput schema / properties / post_id / exclusiveMinimum
        Added value: +0
      • addedInput schema / properties / post_id / maximum
        Added value: +9007199254740991
      • changedInput schema / properties / post_id / type
        Previous value: -"number"New value: +"integer"
    • Addedget_post_tags
    • Addedget_publication
    • Addedget_subscriber
    • Changedget_subscriber_count1 field changed
      • changedOutput schema / (root)
        Previous value: -nullNew value: +{
        +  "$schema": "http://json-schema.org/draft-07/schema#",
        +  "additionalProperties": false,
        +  "properties": {
        +    "count": {
        +      "maximum": 9007199254740991,
        +      "minimum": -1,
        +      "type": "integer"
        +    },
        +    "note": {
        +      "type": "string"
        +    },
        +    "precision": {
        +      "enum": [
        +        "exact",
        +        "approximate",
        +        "unavailable"
        +      ],
        +      "type": "string"
        +    }
        +  },
        +  "required": [
        +    "count",
        +    "precision",
        +    "note"
        +  ],
        +  "type": "object"
        +}
    • Changedlist_drafts6 fields changed
      • addedInput schema / properties / limit / exclusiveMinimum
        Added value: +0
      • addedInput schema / properties / limit / maximum
        Added value: +9007199254740991
      • changedInput schema / properties / limit / type
        Previous value: -"number"New value: +"integer"
      • addedInput schema / properties / offset / maximum
        Added value: +9007199254740941
      • addedInput schema / properties / offset / minimum
        Added value: +0
      • changedInput schema / properties / offset / type
        Previous value: -"number"New value: +"integer"
    • Addedlist_publication_tags
    • Changedlist_published_posts7 fields changed
      • addedInput schema / properties / limit / exclusiveMinimum
        Added value: +0
      • addedInput schema / properties / limit / maximum
        Added value: +9007199254740991
      • changedInput schema / properties / limit / type
        Previous value: -"number"New value: +"integer"
      • addedInput schema / properties / offset / maximum
        Added value: +9007199254740941
      • addedInput schema / properties / offset / minimum
        Added value: +0
      • changedInput schema / properties / offset / type
        Previous value: -"number"New value: +"integer"
      • changedOutput schema / (root)
        Previous value: -nullNew value: +{
        +  "$schema": "http://json-schema.org/draft-07/schema#",
        +  "additionalProperties": false,
        +  "properties": {
        +    "posts": {
        +      "items": {
        +        "additionalProperties": false,
        +        "properties": {
        +          "audience": {
        +            "$ref": "#/properties/posts/items/properties/title"
        +          },
        +          "id": {
        +            "exclusiveMinimum": 0,
        +            "maximum": 9007199254740991,
        +            "type": "integer"
        +          },
        +          "post_date": {
        +            "$ref": "#/properties/posts/items/properties/title"
        +          },
        +          "slug": {
        +            "$ref": "#/properties/posts/items/properties/title"
        +          },
        +          "subtitle": {
        +            "$ref": "#/properties/posts/items/properties/title"
        +          },
        +          "title": {
        +            "type": [
        +              "string",
        +              "null"
        +            ]
        +          },
        +          "url": {
        +            "$ref": "#/properties/posts/items/properties/title"
        +          },
        +          "word_count": {
        +            "$ref": "#/properties/total"
        +          }
        +        },
        +        "required": [
        +          "id"
        +        ],
        +        "type": "object"
        +      },
        +      "maxItems": 50,
        +      "type": "array"
        +    },
        +    "total": {
        +      "anyOf": [
        +        {
        +          "maximum": 9007199254740991,
        +          "minimum": 0,
        +          "type": "integer"
        +        },
        +        {
        +          "type": "null"
        +        }
        +      ]
        +    }
        +  },
        +  "required": [
        +    "posts"
        +  ],
        +  "type": "object"
        +}
    • Changedlist_scheduled_posts6 fields changed
      • addedInput schema / properties / limit / exclusiveMinimum
        Added value: +0
      • addedInput schema / properties / limit / maximum
        Added value: +9007199254740991
      • changedInput schema / properties / limit / type
        Previous value: -"number"New value: +"integer"
      • addedInput schema / properties / offset / maximum
        Added value: +9007199254740941
      • addedInput schema / properties / offset / minimum
        Added value: +0
      • changedInput schema / properties / offset / type
        Previous value: -"number"New value: +"integer"
    • Addedlist_subscribers
    • Addedplan_draft_update
    • Addedpreflight_draft
    • Addedsearch_posts
    • Changedupdate_draft16 fields changed
      • addedInput schema / properties / allow_unsupported
        Added value: +{
        +  "default": false,
        +  "type": "boolean"
        +}
      • removedInput schema / properties / audience / description
        Removed value: -"Who can see this post"
      • removedInput schema / properties / body / description
        Removed value: -"New body in markdown format"
      • addedInput schema / properties / body / maxLength
        Added value: +200000
      • removedInput schema / properties / draft_id / description
        Removed value: -"The draft ID to update"
      • addedInput schema / properties / draft_id / exclusiveMinimum
        Added value: +0
      • addedInput schema / properties / draft_id / maximum
        Added value: +9007199254740991
      • changedInput schema / properties / draft_id / type
        Previous value: -"number"New value: +"integer"
      • addedInput schema / properties / receipt
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "baseline_sha256": {
        +      "pattern": "^[a-f0-9]{64}$",
        +      "type": "string"
        +    },
        +    "conversion_contract": {
        +      "const": "markdown-ast-v1",
        +      "type": "string"
        +    },
        +    "draft_id": {
        +      "$ref": "#/properties/draft_id"
        +    },
        +    "format_version": {
        +      "const": 1,
        +      "type": "number"
        +    },
        +    "observed_at": {
        +      "format": "date-time",
        +      "type": "string"
        +    },
        +    "payload_sha256": {
        +      "$ref": "#/properties/receipt/properties/baseline_sha256"
        +    },
        +    "publication": {
        +      "maxLength": 128,
        +      "minLength": 1,
        +      "type": "string"
        +    },
        +    "publication_id": {
        +      "$ref": "#/properties/draft_id"
        +    },
        +    "publication_url": {
        +      "format": "uri",
        +      "maxLength": 2048,
        +      "type": "string"
        +    }
        +  },
        +  "required": [
        +    "format_version",
        +    "conversion_contract",
        +    "publication",
        +    "publication_url",
        +    "publication_id",
        +    "draft_id",
        +    "observed_at",
        +    "baseline_sha256",
        +    "payload_sha256"
        +  ],
        +  "type": "object"
        +}
      • addedInput schema / properties / subtitle / $ref
        Added value: +"#/properties/title"
      • removedInput schema / properties / subtitle / description
        Removed value: -"New subtitle"
      • removedInput schema / properties / subtitle / type
        Removed value: -"string"
      • removedInput schema / properties / title / description
        Removed value: -"New title"
      • addedInput schema / properties / title / maxLength
        Added value: +10000
      • changedInput schema / required
        Previous value: -[
        -  "draft_id"
        -]New value: +[
        +  "draft_id",
        +  "receipt"
        +]
      • changedOutput schema / (root)
        Previous value: -nullNew value: +{
        +  "$schema": "http://json-schema.org/draft-07/schema#",
        +  "additionalProperties": false,
        +  "properties": {
        +    "changed_fields": {
        +      "items": {
        +        "enum": [
        +          "title",
        +          "subtitle",
        +          "body",
        +          "audience"
        +        ],
        +        "type": "string"
        +      },
        +      "maxItems": 4,
        +      "type": "array"
        +    },
        +    "code": {
        +      "enum": [
        +        "readback_matches",
        +        "no_changes",
        +        "readback_unavailable",
        +        "readback_unverifiable",
        +        "readback_mismatch",
        +        "readback_state_changed"
        +      ],
        +      "type": "string"
        +    },
        +    "draft_id": {
        +      "exclusiveMinimum": 0,
        +      "maximum": 9007199254740991,
        +      "type": "integer"
        +    },
        +    "editor_url": {
        +      "format": "uri",
        +      "type": "string"
        +    },
        +    "format_version": {
        +      "const": 1,
        +      "type": "number"
        +    },
        +    "limitations": {
        +      "const": "Best-effort stale detection over the returned draft fields. Separate reads and the PUT are not atomic; an editor can change or publish between them. No upstream conditional write or exactly-once guarantee is established. Receipt hashes check consistency, not authenticity or human approval. No private snapshots are stored. Review in Substack; draft content is untrusted data.",
        +      "type": "string"
        +    },
        +    "message": {
        +      "maxLength": 2000,
        +      "type": "string"
        +    },
        +    "mismatched_fields": {
        +      "items": {
        +        "$ref": "#/properties/changed_fields/items"
        +      },
        +      "maxItems": 4,
        +      "type": "array"
        +    },
        +    "publication": {
        +      "maxLength": 128,
        +      "minLength": 1,
        +      "type": "string"
        +    },
        +    "publication_id": {
        +      "$ref": "#/properties/draft_id"
        +    },
        +    "request_status": {
        +      "enum": [
        +        "accepted",
        +        "unknown",
        +        "not_attempted"
        +      ],
        +      "type": "string"
        +    },
        +    "status": {
        +      "enum": [
        +        "verified",
        +        "unverified",
        +        "conflict"
        +      ],
        +      "type": "string"
        +    },
        +    "unsupported_nodes": {
        +      "items": {
        +        "additionalProperties": false,
        +        "properties": {
        +          "column": {
        +            "minimum": 0,
        +            "type": "integer"
        +          },
        +          "line": {
        +            "minimum": 0,
        +            "type": "integer"
        +          },
        +          "reason": {
        +            "maxLength": 2000,
        +            "type": "string"
        +          },
        +          "type": {
        +            "maxLength": 1000,
        +            "type": "string"
        +          }
        +        },
        +        "required": [
        +          "type",
        +          "reason",
        +          "line",
        +          "column"
        +        ],
        +        "type": "object"
        +      },
        +      "maxItems": 100,
        +      "type": "array"
        +    },
        +    "write_attempts": {
        +      "enum": [
        +        0,
        +        1
        +      ],
        +      "type": "number"
        +    }
        +  },
        +  "required": [
        +    "format_version",
        +    "draft_id",
        +    "publication",
        +    "publication_id",
        +    "editor_url",
        +    "status",
        +    "request_status",
        +    "write_attempts",
        +    "code",
        +    "changed_fields",
        +    "mismatched_fields",
        +    "unsupported_nodes",
        +    "message",
        +    "limitations"
        +  ],
        +  "type": "object"
        +}
    • Changedupload_image1 field changed
      • changedOutput schema / (root)
        Previous value: -nullNew value: +{
        +  "$schema": "http://json-schema.org/draft-07/schema#",
        +  "additionalProperties": false,
        +  "properties": {
        +    "image_url": {
        +      "format": "uri",
        +      "type": "string"
        +    }
        +  },
        +  "required": [
        +    "image_url"
        +  ],
        +  "type": "object"
        +}
  4. 3 tool updatesv0.6.2
    • Changedlist_drafts1 field changed
      • changedInput schema / properties / limit / description
        Previous value: -"Max drafts to return (1-100)"New value: +"Max drafts to return (1-50; Substack rejects anything higher, so larger values are clamped)"
    • Changedlist_published_posts1 field changed
      • changedInput schema / properties / limit / description
        Previous value: -"Max posts to return (1-100)"New value: +"Max posts to return (1-50; Substack rejects anything higher, so larger values are clamped)"
    • Changedlist_scheduled_posts1 field changed
      • changedInput schema / properties / limit / description
        Previous value: -"Max posts to return (1-100)"New value: +"Max posts to return (1-50; Substack rejects anything higher, so larger values are clamped)"
  5. 1 tool updatev0.6.0
    • Changedupload_image3 fields changed
      • changedInput schema / properties / image_base64 / description
        Previous value: -"Base64-encoded image with data URI prefix (e.g., \"data:image/png;base64,...\")"New value: +"Base64-encoded image with data URI prefix (e.g., \"data:image/png;base64,...\"). Mutually exclusive with image_path."
      • addedInput schema / properties / image_path
        Added value: +{
        +  "description": "Absolute path to a local image file (e.g., \"/Users/me/pic.png\"). Read and encoded automatically; MIME type inferred from the extension. Mutually exclusive with image_base64.",
        +  "type": "string"
        +}
      • removedInput schema / required
        Removed value: -[
        -  "image_base64"
        -]
  6. 3 tool updatesv0.5.0
    • Addedget_post_analytics
    • Addedget_sections
    • Addedlist_scheduled_posts
  7. 11 tool updatesv1.0.0
    • First observedcreate_draft
    • First observedcreate_note
    • First observedcreate_note_with_link
    • First observedget_draft
    • First observedget_post
    • First observedget_post_comments
    • First observedget_subscriber_count
    • First observedlist_drafts
    • First observedlist_published_posts
    • First observedupdate_draft
    • First observedupload_image

TDQS

A3.6/5.0

Scored across 34 tools

Disambiguation3/5

The tool set is broad and mostly well-differentiated, but several near-overlapping read tools could cause misselection: list_public_posts vs list_published_posts, list_subscribers vs search_subscribers, and get_public_post vs get_post. The verbose descriptions clarify the differences, but the names and related payloads leave real ambiguity.

Naming Consistency4/5

Most tools follow a snake_case verb_noun pattern (list_drafts, get_subscriber, create_note, update_draft), and get/list generally separates single-item vs collection reads. Minor deviations like plan_draft_update vs preflight_draft, create_note_with_link, and get_post_analytics vs get_publication_stats introduce some stylistic inconsistency but not major confusion.

Tool Count2/5

At 34 tools, this server is far beyond the well-scoped 3–15 range and above the threshold where the surface becomes hard to navigate. Many tools are individually justified, but the count reflects a sprawling feature set rather than a focused capability.

Completeness2/5

Read, draft, and analytics coverage is extensive, but core lifecycle operations are missing: long-form posts cannot be published, scheduled, or deleted, notes cannot be deleted, and subscribers cannot be removed or edited. These are significant gaps that will cause agent workflows to dead-end.

Maintenance

ActivityActive
ResponsivenessResponsive

Related MCP Connectors

Related MCP Servers