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
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.5",
"version": "0.3.6",
"description": "Unofficial, portable TypeScript SDK for Substack's web API",
"type": "module",
"license": "MIT",
Expand Down
8 changes: 8 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,12 +476,20 @@ export type UnreadActivityFeed = ActivityFeed & {
export interface GrowthSourcesOptions {
/** Start date in YYYY-MM-DD format. */
fromDate?: string
/** Start date alias (snake_case). */
from_date?: string
/** End date in YYYY-MM-DD format. */
toDate?: string
/** End date alias (snake_case). */
to_date?: string
/** Upstream metric to order by. Defaults to `users`. */
orderBy?: 'users' | 'subscriptions' | 'annual_subscriptions' | 'revenue' | string
/** Upstream metric to order by alias (snake_case). */
order_by?: 'users' | 'subscriptions' | 'annual_subscriptions' | 'revenue' | string
/** Sort direction. Defaults to `desc`. */
orderDirection?: 'asc' | 'desc'
/** Sort direction alias (snake_case). */
order_direction?: 'asc' | 'desc'
}

export interface GrowthMetricTimeseriesPoint {
Expand Down
24 changes: 15 additions & 9 deletions src/mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,9 @@ export function createToolHandlers(client: ReadOnlyClient) {
},
getGrowthSources: (options: GrowthSourcesOptions = {}) =>
run(async () => {
if (options.fromDate && options.toDate && options.fromDate > options.toDate) {
const fromDate = options.fromDate ?? options.from_date
const toDate = options.toDate ?? options.to_date
if (fromDate && toDate && fromDate > toDate) {
throw new SubstackConfigurationError('Growth sources fromDate cannot be after toDate.')
}
return client.getGrowthSources(options)
Expand All @@ -404,7 +406,7 @@ export function createToolHandlers(client: ReadOnlyClient) {
}

export function createMcpServer(client: ReadOnlyClient): McpServer {
const server = new McpServer({ name: 'substack-mcp', version: '0.3.5' })
const server = new McpServer({ name: 'substack-mcp', version: '0.3.6' })
const tools = createToolHandlers(client)

server.registerTool(
Expand Down Expand Up @@ -624,19 +626,23 @@ export function createMcpServer(client: ReadOnlyClient): McpServer {
'Get historical breakdown of publication traffic, subscriber acquisition, and revenue by referrer / growth channel over a date range.',
inputSchema: {
from_date: date.optional(),
fromDate: date.optional(),
to_date: date.optional(),
order_by: growthSourcesOrderBy,
order_direction: orderDirection
toDate: date.optional(),
order_by: growthSourcesOrderBy.optional(),
orderBy: growthSourcesOrderBy.optional(),
order_direction: orderDirection.optional(),
orderDirection: orderDirection.optional()
},
outputSchema,
annotations: readOnlyAnnotations
},
({ from_date, to_date, order_by, order_direction }) =>
(args: any) =>
tools.getGrowthSources({
fromDate: from_date,
toDate: to_date,
orderBy: order_by,
orderDirection: order_direction
fromDate: args.from_date ?? args.fromDate,
toDate: args.to_date ?? args.toDate,
orderBy: args.order_by ?? args.orderBy ?? 'users',
orderDirection: args.order_direction ?? args.orderDirection ?? 'desc'
})
)

Expand Down
13 changes: 9 additions & 4 deletions src/resources/growth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ import type { GrowthSourcesOptions, GrowthSourcesResponse, GrowthSourceItem } fr

function growthSourcesQuery(options: GrowthSourcesOptions): URLSearchParams {
const params = new URLSearchParams()
params.set('order_by', options.orderBy ?? 'users')
params.set('order_direction', options.orderDirection ?? 'desc')
if (options.fromDate) params.set('from_date', options.fromDate)
if (options.toDate) params.set('to_date', options.toDate)
const orderBy = options.orderBy ?? options.order_by ?? 'users'
const orderDirection = options.orderDirection ?? options.order_direction ?? 'desc'
const fromDate = options.fromDate ?? options.from_date
const toDate = options.toDate ?? options.to_date

params.set('order_by', orderBy)
params.set('order_direction', orderDirection)
if (fromDate) params.set('from_date', fromDate)
if (toDate) params.set('to_date', toDate)
return params
}

Expand Down
11 changes: 11 additions & 0 deletions test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,17 @@ describe('SubstackClient', () => {
expect(requests[0].url).toBe(
'https://allagentsconsidered.substack.com/api/v1/publication/stats/growth/sources?order_by=users&order_direction=desc&from_date=2026-07-29&to_date=2026-08-27'
)

await client.getGrowthSources({
from_date: '2026-03-01',
to_date: '2026-03-31',
order_by: 'subscriptions',
order_direction: 'asc'
})

expect(requests[1].url).toBe(
'https://allagentsconsidered.substack.com/api/v1/publication/stats/growth/sources?order_by=subscriptions&order_direction=asc&from_date=2026-03-01&to_date=2026-03-31'
)
})

test('requires a publication URL for growth sources', () => {
Expand Down
22 changes: 21 additions & 1 deletion test/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ describe('MCP tools', () => {
expect(result.content[0].text).toBe('Failed to fetch subscriber stats: Network timeout')
})

test('returns growth sources structured content from tool handler', async () => {
test('returns growth sources structured content from tool handler with snake_case and camelCase args', async () => {
const tools = createToolHandlers(mockClient() as never)
const result = await tools.getGrowthSources({
fromDate: '2026-07-29',
Expand All @@ -299,6 +299,19 @@ describe('MCP tools', () => {
totals: [{ name: 'traffic', total: 149 }]
}
})

const snakeResult = await tools.getGrowthSources({
from_date: '2026-03-01',
to_date: '2026-03-31',
order_by: 'users'
})
expect(snakeResult.isError).toBeUndefined()
expect(snakeResult.structuredContent).toEqual({
data: {
sourceMetrics: [{ source: 'substack', sourceName: 'Substack' }],
totals: [{ name: 'traffic', total: 149 }]
}
})
})

test('rejects an inverted growth sources date range', async () => {
Expand All @@ -310,6 +323,13 @@ describe('MCP tools', () => {

expect(result.isError).toBe(true)
expect(result.content[0].text).toContain('fromDate cannot be after toDate')

const snakeInverted = await tools.getGrowthSources({
from_date: '2026-08-27',
to_date: '2026-07-29'
})
expect(snakeInverted.isError).toBe(true)
expect(snakeInverted.content[0].text).toContain('fromDate cannot be after toDate')
})

test('caps activity while retaining unread metadata', async () => {
Expand Down