Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Copy [`.dev.vars.example`](.dev.vars.example) to `.dev.vars` and replace the pla

## MCP server

The package includes a read-only STDIO MCP server for publication, post, Note, subscriber, and activity analytics. It exposes both bounded raw endpoint data and compact full-history summaries. Set `SUBSTACK_SESSION_TOKEN` and `SUBSTACK_PUBLICATION_URL`, then configure Codex:
The package includes a read-only STDIO MCP server for publication, post, Note, subscriber, and activity analytics. It returns compact, normalized model-facing data by default, with explicit raw-data opt-ins where supported. Set `SUBSTACK_SESSION_TOKEN` and `SUBSTACK_PUBLICATION_URL`, then configure Codex:

```toml
[mcp_servers.substack]
Expand All @@ -67,15 +67,15 @@ Keep the session token local and out of source control. All MCP tools are read-o
| `get_publication_analytics` | Full-history totals, average upstream rates, audience/section/type breakdowns, top posts, and optional raw rows. |
| `get_post_engagement` | Post content engagement and a bounded visible-comment sample. |
| `get_post_analytics` | Combined author analytics, delivery, conversion, media, links, referrers, comparison data, and visible engagement. |
| `get_notes` | Bounded Notes page from authenticated profile or optional `profile_id`. |
| `get_profile_notes` | Bounded profile Notes page with raw per-Note metrics. |
| `get_notes` | Compact, body-first Notes from the authenticated profile or optional `profile_id`; supports guarded `fetch_all`. |
| `get_profile_notes` | Compact, body-first profile Notes with cursor paging or guarded `fetch_all`. |
| `get_note_engagement` | Reactions, restacks, viewer state, and fully paginated direct/nested reply totals. |
| `get_subscriber_summary` | Privacy-safe subscriber totals. Raw records require explicit `include_records: true`. |
| `get_activity` | Bounded activity filtered by all events, replies and mentions, or restacks. |
| `get_unread_activity` | Bounded unread activity plus unread metadata. |
| `analyze_content` | Compact complete analytics for one post without comment or raw-response payloads. |

`get_publication_analytics` follows every email-stat page before calculating its summary, so it can make several authenticated requests for a large archive. Raw rows are excluded by default and capped when requested. `get_subscriber_summary` excludes subscriber records by default because they can contain email addresses and other personal data. See [MCP analytics](docs/mcp-analytics.md) for output semantics and usage examples.
`get_publication_analytics` follows every email-stat page before calculating its summary, so it can make several authenticated requests for a large archive. Raw rows are excluded by default and capped when requested. `get_notes` and `get_profile_notes` default to 10 complete Note bodies; set `fetch_all: true` to follow every cursor up to `max_items` (default 500, maximum 5,000). `get_subscriber_summary` excludes subscriber records by default because they can contain email addresses and other personal data. See [MCP analytics](docs/mcp-analytics.md) for output semantics and usage examples.

## API

Expand Down
30 changes: 23 additions & 7 deletions docs/mcp-analytics.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MCP analytics

The package's STDIO MCP server is read-only. It uses the same authenticated `SubstackClient` as the SDK and returns each successful result as both JSON text and structured content under `{ data }`.
The package's STDIO MCP server is read-only. It uses the same authenticated `SubstackClient` as the SDK and returns normalized structured content under `{ data }`. The text content is a short result summary rather than a duplicate JSON payload, which keeps model context smaller.

Substack's web API is undocumented and can change without notice. Raw endpoint fields are retained where a tool returns them, while derived fields use explicit names and bounded payloads.

Expand All @@ -15,9 +15,9 @@ The result contains:
- `rowsAnalyzed`: rows remaining after date filtering.
- `dateRange`: earliest and latest included raw `post_date` values.
- `totals`: sums of additive delivery, engagement, conversion, revenue, podcast, and video fields that were present.
- `averageRates`: arithmetic means of Substack's raw `open_rate`, `click_through_rate`, and `engagement_rate` values. These are not recalculated or weighted because Substack does not document every numerator's semantics.
- `summary`: arithmetic means of Substack's raw `open_rate`, `click_through_rate`, and `engagement_rate` values. These are not recalculated or weighted because Substack does not document every numerator's semantics.
- `breakdowns`: post counts grouped by audience, section, and content type.
- `topPosts`: a bounded ranking by the requested metric.
- `top_posts`: a bounded ranking by `top_metric`.
- `availableFields`: every raw field present in the included rows, including future fields unknown to the SDK.
- `rows`: optional raw rows, capped by `row_limit` at 200.

Expand Down Expand Up @@ -50,16 +50,32 @@ Example MCP arguments:

## Note analytics

`get_profile_notes` returns raw per-Note fields from a bounded profile feed page. `get_note_engagement` calls `getNoteWithEngagement()`, follows every reply cursor, and reports normalized reactions, restacks, direct replies, nested replies, total replies, viewer state, and `replyCountsComplete`.
`get_notes` and `get_profile_notes` return compact, body-first records with the complete Note `body`, plus `id`, `created_at`, and an upstream permalink when available. Large tracking, publication, theme, palette, and subscription objects are removed. Minimal attachment details are retained only for Notes without a text body.

Only the returned reply sample is capped. The normalized counts still represent every safely loaded page. `rawReplyPages` is excluded unless `include_raw_pages` is true. Note views remain absent unless a future Substack response includes a numeric `views` or `view_count` field.
Both tools default to 10 Notes and accept `limit` from 1 through 50. Their response includes `returned`, `pages_fetched`, `complete`, `has_more`, and a normalized `cursor`.

Set `fetch_all` to true to follow profile-feed cursors automatically. `max_items` is a safety bound (default 500, maximum 5,000), repeated Note IDs are removed, and repeated cursors stop collection. If the bound is reached, `complete` is false and `cursor` identifies the page from which collection can resume.

Example full-profile request:

```json
{
"profile_id": 44242110,
"fetch_all": true,
"max_items": 500
}
```

`get_note_engagement` calls `getNoteWithEngagement()`, follows every reply cursor, and reports normalized reactions, restacks, direct replies, nested replies, total replies, viewer state, and `replyCountsComplete`.

Only the returned reply sample is capped. The normalized counts still represent every safely loaded page. `raw_reply_pages` is excluded unless `include_raw_pages` is true. Note views remain absent unless a future Substack response includes a numeric `views` or `view_count` field.

## Subscriber privacy

`get_subscriber_summary` returns the upstream aggregate subscriber count when available, the number of records present in the response, numeric and boolean top-level aggregates, and available record field names without exposing record values. This is the default behavior.

Setting `include_records` to true returns up to `record_limit` raw subscriber records and sets `personalDataIncluded` to true. Those records can contain email addresses and other personal data. Use the option only in a trusted local MCP session and do not copy its results into logs, prompts, issues, or source control.
Setting `include_records` to true returns up to `record_limit` raw subscriber records and sets `personal_data_included` to true. Those records can contain email addresses and other personal data. Use the option only in a trusted local MCP session and do not copy its results into logs, prompts, issues, or source control.

## Response limits

List-returning tools cap data returned to the model while retaining upstream pagination metadata when available. The caps reduce context size; they do not change Substack's fixed email-stat page size or the complete pagination needed for publication and Note summaries.
List-returning tools cap and normalize data returned to the model while retaining pagination metadata when available. Activity tools discard large upstream dependency tables and tracking parameters. These reductions do not change Substack's fixed email-stat page size or the complete pagination used for publication analytics and guarded full-profile Note collection.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "unofficial-substack-sdk",
"version": "0.3.9",
"version": "0.3.10",
"description": "Unofficial, portable TypeScript SDK for Substack's web API",
"type": "module",
"license": "MIT",
Expand Down
3 changes: 2 additions & 1 deletion src/core/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type {
PostManagementPost,
PostWithEngagement,
PostWithEngagementOptions,
ProfileNotesOptions,
ProfileNotesPage,
PublishNoteRequest,
ProfilePostsOptions,
Expand Down Expand Up @@ -207,7 +208,7 @@ export class SubstackClient {

getProfileNotes<T extends Record<string, unknown> = NoteFeedItem>(
id: number | string,
options: CursorOptions = {}
options: ProfileNotesOptions = {}
): Promise<ProfileNotesPage<T>> {
return getProfileNotes<T>(this.endpoints, id, options)
}
Expand Down
1 change: 1 addition & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export {
type PostWithEngagement,
type PostWithEngagementOptions,
type ProfileNoteItem,
type ProfileNotesOptions,
type ProfileNotesPage,
type PublishNoteRequest,
type ProfilePostsOptions,
Expand Down
7 changes: 6 additions & 1 deletion src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ export interface CursorOptions {
cursor?: string
}

export interface NotesOptions extends CursorOptions {
export interface ProfileNotesOptions extends CursorOptions {
/** Requested upstream page size. */
limit?: number
}

export interface NotesOptions extends ProfileNotesOptions {
profileId?: number | string
}

Expand Down
Loading