From 4e118fd9bb7ef1190ab373d3b0db8758df5de480 Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 27 Aug 2026 15:28:17 +0300 Subject: [PATCH] fix: support snake_case and camelCase date parameters for growth sources --- package.json | 2 +- src/core/types.ts | 8 ++++++++ src/mcp-server.ts | 24 +++++++++++++++--------- src/resources/growth/index.ts | 13 +++++++++---- test/client.test.ts | 11 +++++++++++ test/mcp.test.ts | 22 +++++++++++++++++++++- 6 files changed, 65 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index d636e7e..a4865be 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/core/types.ts b/src/core/types.ts index 7d03420..16447ed 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -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 { diff --git a/src/mcp-server.ts b/src/mcp-server.ts index c04a197..0a9c3fc 100644 --- a/src/mcp-server.ts +++ b/src/mcp-server.ts @@ -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) @@ -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( @@ -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' }) ) diff --git a/src/resources/growth/index.ts b/src/resources/growth/index.ts index 1bd49dc..b419d0c 100644 --- a/src/resources/growth/index.ts +++ b/src/resources/growth/index.ts @@ -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 } diff --git a/test/client.test.ts b/test/client.test.ts index 18e8dca..1e5f03d 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -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', () => { diff --git a/test/mcp.test.ts b/test/mcp.test.ts index f15b0df..78bc952 100644 --- a/test/mcp.test.ts +++ b/test/mcp.test.ts @@ -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', @@ -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 () => { @@ -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 () => {