posterly-mcp-server 0.48.5 → 0.49.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +9 -3
  2. package/dist/generated/platform-manifest.d.ts +3 -3
  3. package/dist/generated/platform-manifest.js +3 -2
  4. package/dist/lib/api-client.d.ts +80 -12
  5. package/dist/lib/api-client.js +40 -15
  6. package/dist/lib/format.d.ts +39 -2
  7. package/dist/lib/format.js +158 -0
  8. package/dist/lib/version.d.ts +1 -1
  9. package/dist/lib/version.js +1 -1
  10. package/dist/tools/audit-google-business-profile.d.ts +2 -2
  11. package/dist/tools/connect-account.d.ts +2 -2
  12. package/dist/tools/create-post.d.ts +12 -12
  13. package/dist/tools/create-posts-batch.d.ts +8 -8
  14. package/dist/tools/create-webhook.d.ts +2 -2
  15. package/dist/tools/disconnect-account.d.ts +2 -2
  16. package/dist/tools/find-slot.d.ts +2 -2
  17. package/dist/tools/get-updates.d.ts +4 -0
  18. package/dist/tools/get-updates.js +13 -1
  19. package/dist/tools/list-accounts.d.ts +2 -2
  20. package/dist/tools/list-activity.d.ts +4 -0
  21. package/dist/tools/list-activity.js +16 -3
  22. package/dist/tools/list-analytics.d.ts +2 -2
  23. package/dist/tools/list-analytics.js +1 -1
  24. package/dist/tools/list-comments.d.ts +2 -2
  25. package/dist/tools/list-conversations.d.ts +2 -2
  26. package/dist/tools/list-google-business-media.d.ts +2 -2
  27. package/dist/tools/list-google-business-reviews.d.ts +4 -4
  28. package/dist/tools/list-media.d.ts +4 -0
  29. package/dist/tools/list-media.js +22 -1
  30. package/dist/tools/list-platforms.d.ts +4 -4
  31. package/dist/tools/list-post-suggestions.d.ts +4 -0
  32. package/dist/tools/list-post-suggestions.js +16 -4
  33. package/dist/tools/list-posts.d.ts +35 -17
  34. package/dist/tools/list-posts.js +63 -33
  35. package/dist/tools/manage-comment.d.ts +2 -2
  36. package/dist/tools/manage-conversation.d.ts +4 -4
  37. package/dist/tools/manage-google-business-media.d.ts +8 -8
  38. package/dist/tools/manage-google-business-review.d.ts +10 -10
  39. package/dist/tools/manage-publishing-pause.d.ts +2 -2
  40. package/dist/tools/manage-webhook.d.ts +4 -4
  41. package/dist/tools/validate-post.d.ts +8 -8
  42. package/package.json +1 -1
  43. package/server.json +2 -2
  44. package/src/generated/platform-manifest.ts +3 -2
  45. package/src/lib/api-client.ts +94 -17
  46. package/src/lib/format.ts +201 -1
  47. package/src/lib/version.ts +1 -1
  48. package/src/tools/get-updates.ts +15 -1
  49. package/src/tools/list-activity.ts +18 -3
  50. package/src/tools/list-analytics.ts +1 -1
  51. package/src/tools/list-media.ts +23 -2
  52. package/src/tools/list-post-suggestions.ts +17 -4
  53. package/src/tools/list-posts.ts +74 -45
  54. package/tool-registry.json +110 -5
package/README.md CHANGED
@@ -123,7 +123,7 @@ Add the same server definition to your Cursor MCP settings:
123
123
 
124
124
  ## Available tools
125
125
 
126
- `posterly-mcp-server@0.48.5` exposes 64 tools covering 99 actions. The hosted HTTP endpoint exposes the same catalog minus the three signup tools: 61 tools covering 96 actions.
126
+ `posterly-mcp-server@0.49.1` exposes 64 tools covering 100 actions. The hosted HTTP endpoint exposes the same catalog minus the three signup tools: 61 tools covering 97 actions.
127
127
 
128
128
  Many tools pick what to do with one parameter: `view` (reads), `action` (writes), or `type`. A few pick their mode from which parameters you pass (for example `update_post` with `status` changes the status instead of editing). Every tool description names all of its views and actions.
129
129
 
@@ -147,7 +147,6 @@ Accounts, posting, media, schedule, analytics, and support. What most assistants
147
147
  | `connect_account` | Connect with credentials |
148
148
  | `create_connect_session` | Start a connect session |
149
149
  | `trigger_platform_helper` | Run a platform helper |
150
- | `list_posts` | Posts |
151
150
  | `ask_support` | Ask support |
152
151
  | `find_available_slot` | Find a free slot |
153
152
  | `submit_agent_feedback` | Agent feedback |
@@ -197,6 +196,13 @@ Accounts, posting, media, schedule, analytics, and support. What most assistants
197
196
  | `accounts` | Brand accounts |
198
197
  | `profile` | Brand profile |
199
198
 
199
+ **`list_posts`**: Posts
200
+
201
+ | View | What it does |
202
+ | --- | --- |
203
+ | `list` (default) | Posts, one page at a time |
204
+ | `counts` | Post counts by day, status, account, and brand |
205
+
200
206
  **`get_post`**: One post
201
207
 
202
208
  | View | What it does |
@@ -389,7 +395,7 @@ Pre-auth signup for brand-new users. Local npm package only; always on.
389
395
 
390
396
  > Google Business tip: pass `account_id` (the integer `id` from `list_accounts`) to every Google Business tool. If you use `location_id` instead, it is the numeric `location_id` from `list_accounts` (the GBP location id), not the `ChIJ...` Place ID that `list_google_business_reviews` with `view: "review_link"` returns. Passing the Place ID resolves to no account and returns "Google Business Profile account not found".
391
397
 
392
- Analytics (`list_analytics`) currently supports Instagram, Facebook Pages, LinkedIn, Google Business Profile, Pinterest, YouTube, and Threads.
398
+ Analytics (`list_analytics`) currently supports Instagram, X, Facebook Pages, LinkedIn, Google Business Profile, Pinterest, YouTube, and Threads.
393
399
 
394
400
  ## Renamed tools
395
401
 
@@ -34,7 +34,7 @@ export declare const PLATFORM_MANIFEST: {
34
34
  readonly youtube: "youtube";
35
35
  };
36
36
  readonly helperNames: readonly ["linkedin.recent_mentions", "pinterest.boards", "tiktok.creator_info", "x.quota"];
37
- readonly analyticsPlatformIds: readonly ["instagram", "facebook", "linkedin", "youtube", "pinterest", "threads", "google_business"];
37
+ readonly analyticsPlatformIds: readonly ["instagram", "facebook", "twitter", "linkedin", "youtube", "pinterest", "threads", "google_business"];
38
38
  readonly connectTargetIds: readonly ["instagram", "meta", "facebook", "youtube", "tiktok", "linkedin", "linkedin_page", "threads", "google_business", "pinterest", "twitter", "telegram", "bluesky", "discord", "slack", "mastodon", "devto", "hashnode", "wordpress", "lemmy", "reddit", "medium", "skool", "whop"];
39
39
  readonly connectInputs: readonly ["bluesky", "devto", "discord", "facebook", "facebook_instagram", "facebook_pages", "gmb", "google-business", "google_business", "google_business_profile", "hashnode", "instagram", "instagram-standalone", "instagram_direct", "lemmy", "linkedin", "linkedin-company", "linkedin-page", "linkedin_company", "linkedin_page", "linkedin_personal", "mastodon", "medium", "meta", "meta_business", "pinterest", "reddit", "skool", "slack", "telegram", "threads", "tiktok", "twitter", "whop", "wordpress", "x", "x_twitter", "youtube"];
40
40
  readonly connectAliases: {
@@ -104,7 +104,7 @@ export declare const PLATFORM_MANIFEST: {
104
104
  readonly status: "supported";
105
105
  readonly aliases: readonly ["x"];
106
106
  readonly postTypes: readonly ["text", "image", "video", "carousel", "x_thread"];
107
- readonly analyticsSupported: false;
107
+ readonly analyticsSupported: true;
108
108
  readonly helpers: readonly ["x.quota"];
109
109
  }, {
110
110
  readonly id: "linkedin";
@@ -405,4 +405,4 @@ export declare const ALL_PLATFORM_INPUTS: readonly ["bluesky", "devto", "discord
405
405
  export declare const PLATFORM_HELPER_NAMES: readonly ["linkedin.recent_mentions", "pinterest.boards", "tiktok.creator_info", "x.quota"];
406
406
  export declare const CONNECT_TARGET_IDS: readonly ["instagram", "meta", "facebook", "youtube", "tiktok", "linkedin", "linkedin_page", "threads", "google_business", "pinterest", "twitter", "telegram", "bluesky", "discord", "slack", "mastodon", "devto", "hashnode", "wordpress", "lemmy", "reddit", "medium", "skool", "whop"];
407
407
  export declare const CONNECT_INPUTS: readonly ["bluesky", "devto", "discord", "facebook", "facebook_instagram", "facebook_pages", "gmb", "google-business", "google_business", "google_business_profile", "hashnode", "instagram", "instagram-standalone", "instagram_direct", "lemmy", "linkedin", "linkedin-company", "linkedin-page", "linkedin_company", "linkedin_page", "linkedin_personal", "mastodon", "medium", "meta", "meta_business", "pinterest", "reddit", "skool", "slack", "telegram", "threads", "tiktok", "twitter", "whop", "wordpress", "x", "x_twitter", "youtube"];
408
- export declare const ANALYTICS_PLATFORM_IDS: readonly ["instagram", "facebook", "linkedin", "youtube", "pinterest", "threads", "google_business"];
408
+ export declare const ANALYTICS_PLATFORM_IDS: readonly ["instagram", "facebook", "twitter", "linkedin", "youtube", "pinterest", "threads", "google_business"];
@@ -142,6 +142,7 @@ export const PLATFORM_MANIFEST = {
142
142
  "analyticsPlatformIds": [
143
143
  "instagram",
144
144
  "facebook",
145
+ "twitter",
145
146
  "linkedin",
146
147
  "youtube",
147
148
  "pinterest",
@@ -315,7 +316,7 @@ export const PLATFORM_MANIFEST = {
315
316
  "carousel",
316
317
  "x_thread"
317
318
  ],
318
- "analyticsSupported": false,
319
+ "analyticsSupported": true,
319
320
  "helpers": [
320
321
  "x.quota"
321
322
  ]
@@ -749,4 +750,4 @@ export const ALL_PLATFORM_INPUTS = ["bluesky", "devto", "discord", "facebook", "
749
750
  export const PLATFORM_HELPER_NAMES = ["linkedin.recent_mentions", "pinterest.boards", "tiktok.creator_info", "x.quota"];
750
751
  export const CONNECT_TARGET_IDS = ["instagram", "meta", "facebook", "youtube", "tiktok", "linkedin", "linkedin_page", "threads", "google_business", "pinterest", "twitter", "telegram", "bluesky", "discord", "slack", "mastodon", "devto", "hashnode", "wordpress", "lemmy", "reddit", "medium", "skool", "whop"];
751
752
  export const CONNECT_INPUTS = ["bluesky", "devto", "discord", "facebook", "facebook_instagram", "facebook_pages", "gmb", "google-business", "google_business", "google_business_profile", "hashnode", "instagram", "instagram-standalone", "instagram_direct", "lemmy", "linkedin", "linkedin-company", "linkedin-page", "linkedin_company", "linkedin_page", "linkedin_personal", "mastodon", "medium", "meta", "meta_business", "pinterest", "reddit", "skool", "slack", "telegram", "threads", "tiktok", "twitter", "whop", "wordpress", "x", "x_twitter", "youtube"];
752
- export const ANALYTICS_PLATFORM_IDS = ["instagram", "facebook", "linkedin", "youtube", "pinterest", "threads", "google_business"];
753
+ export const ANALYTICS_PLATFORM_IDS = ["instagram", "facebook", "twitter", "linkedin", "youtube", "pinterest", "threads", "google_business"];
@@ -134,6 +134,11 @@ export interface PostSuggestion {
134
134
  }
135
135
  export interface PostSuggestionsResponse {
136
136
  suggestions: PostSuggestion[];
137
+ total?: number;
138
+ limit?: number;
139
+ offset?: number;
140
+ has_more?: boolean;
141
+ next_offset?: number | null;
137
142
  }
138
143
  export interface DismissSuggestionResponse {
139
144
  ok: boolean;
@@ -169,6 +174,59 @@ export interface Post {
169
174
  content_plan_id?: string | null;
170
175
  approval?: ApiPostApproval;
171
176
  }
177
+ /** Filters shared by GET /posts and GET /posts/counts. */
178
+ export interface ListPostsParams {
179
+ status?: string;
180
+ platform?: string;
181
+ account_id?: string;
182
+ workspace_id?: string;
183
+ approval_status?: string;
184
+ approval_waiting_on?: string;
185
+ brand_id?: string;
186
+ scheduled_from?: string;
187
+ scheduled_to?: string;
188
+ timezone?: string;
189
+ }
190
+ export interface PostsRangeEcho {
191
+ from: string | null;
192
+ to: string | null;
193
+ timezone: string;
194
+ }
195
+ export interface ListPostsResponse {
196
+ posts: Post[];
197
+ total: number;
198
+ limit?: number;
199
+ offset?: number;
200
+ has_more?: boolean;
201
+ next_offset?: number | null;
202
+ order?: 'asc' | 'desc';
203
+ range?: PostsRangeEcho | null;
204
+ }
205
+ export interface PostCountsResponse {
206
+ total: number;
207
+ counted: number;
208
+ truncated: boolean;
209
+ range: PostsRangeEcho | null;
210
+ by_day: Array<{
211
+ date: string;
212
+ count: number;
213
+ }>;
214
+ by_status: Array<{
215
+ status: string;
216
+ count: number;
217
+ }>;
218
+ by_account: Array<{
219
+ account_id: string | null;
220
+ platform: string | null;
221
+ username: string | null;
222
+ count: number;
223
+ }>;
224
+ by_brand: Array<{
225
+ brand_id: string | null;
226
+ name: string | null;
227
+ count: number;
228
+ }>;
229
+ }
172
230
  export interface InstagramPostSettings {
173
231
  __type?: 'instagram' | 'instagram-standalone';
174
232
  post_type?: 'post' | 'feed' | 'story' | 'reel' | 'carousel';
@@ -891,8 +949,12 @@ export interface ActivityItem {
891
949
  }
892
950
  export interface ActivityResponse {
893
951
  items: ActivityItem[];
952
+ /** Items on this page (kept for older clients); use has_more to tell whether more exist. */
894
953
  total: number;
895
954
  limit: number;
955
+ offset?: number;
956
+ has_more?: boolean;
957
+ next_offset?: number | null;
896
958
  }
897
959
  export interface UpdateEntry {
898
960
  slug: string;
@@ -909,6 +971,11 @@ export interface UpdateEntry {
909
971
  }
910
972
  export interface UpdatesResponse {
911
973
  updates: UpdateEntry[];
974
+ total?: number;
975
+ limit?: number;
976
+ offset?: number;
977
+ has_more?: boolean;
978
+ next_offset?: number | null;
912
979
  }
913
980
  export interface ApiWebhook {
914
981
  id: string;
@@ -1316,22 +1383,16 @@ export declare class PosterlyClient {
1316
1383
  account_id?: string | number;
1317
1384
  status?: 'pending' | 'scheduled' | 'dismissed';
1318
1385
  limit?: number;
1386
+ offset?: number;
1319
1387
  }): Promise<PostSuggestionsResponse>;
1320
1388
  dismissSuggestion(id: string): Promise<DismissSuggestionResponse>;
1321
- listPosts(params?: {
1322
- status?: string;
1323
- platform?: string;
1324
- account_id?: string;
1389
+ listPosts(params?: ListPostsParams & {
1325
1390
  limit?: number;
1326
1391
  offset?: number;
1327
- workspace_id?: string;
1328
- approval_status?: string;
1329
- approval_waiting_on?: string;
1330
- brand_id?: string;
1331
- }): Promise<{
1332
- posts: Post[];
1333
- total: number;
1334
- }>;
1392
+ order?: 'asc' | 'desc';
1393
+ }): Promise<ListPostsResponse>;
1394
+ /** GET /posts/counts: exact totals per day, status, account, and brand for a scheduled range. */
1395
+ countPosts(params: ListPostsParams): Promise<PostCountsResponse>;
1335
1396
  createPost(data: CreatePostPayload): Promise<CreatePostResponse>;
1336
1397
  validatePost(data: CreatePostPayload): Promise<ValidatePostResponse>;
1337
1398
  submitAgentFeedback(data: AgentFeedbackPayload): Promise<AgentFeedbackResponse>;
@@ -1543,9 +1604,11 @@ export declare class PosterlyClient {
1543
1604
  since?: string;
1544
1605
  type?: 'all' | 'activity' | 'publish';
1545
1606
  limit?: number;
1607
+ offset?: number;
1546
1608
  }): Promise<ActivityResponse>;
1547
1609
  getUpdates(params?: {
1548
1610
  limit?: number;
1611
+ offset?: number;
1549
1612
  since?: string;
1550
1613
  include_content?: boolean;
1551
1614
  }): Promise<UpdatesResponse>;
@@ -1624,6 +1687,7 @@ export declare class PosterlyClient {
1624
1687
  }>;
1625
1688
  listMedia(params?: {
1626
1689
  limit?: number;
1690
+ offset?: number;
1627
1691
  dropSessionId?: string;
1628
1692
  }): Promise<{
1629
1693
  media: Array<{
@@ -1637,6 +1701,10 @@ export declare class PosterlyClient {
1637
1701
  drop_session_id: string | null;
1638
1702
  }>;
1639
1703
  total: number;
1704
+ limit?: number;
1705
+ offset?: number;
1706
+ has_more?: boolean;
1707
+ next_offset?: number | null;
1640
1708
  }>;
1641
1709
  uploadMedia(input: {
1642
1710
  filePath?: string;
@@ -36,6 +36,30 @@ export function setMcpClientNameResolver(resolver) {
36
36
  export function sanitizeClientHeaderValue(value) {
37
37
  return value.replace(/[^\x20-\x7e]/g, '').trim().slice(0, 200);
38
38
  }
39
+ function postFilterParams(params) {
40
+ const searchParams = new URLSearchParams();
41
+ if (params?.status)
42
+ searchParams.set('status', params.status);
43
+ if (params?.platform)
44
+ searchParams.set('platform', params.platform);
45
+ if (params?.account_id)
46
+ searchParams.set('account_id', params.account_id);
47
+ if (params?.workspace_id)
48
+ searchParams.set('workspace_id', params.workspace_id);
49
+ if (params?.approval_status)
50
+ searchParams.set('approval_status', params.approval_status);
51
+ if (params?.approval_waiting_on)
52
+ searchParams.set('approval_waiting_on', params.approval_waiting_on);
53
+ if (params?.brand_id)
54
+ searchParams.set('brand_id', params.brand_id);
55
+ if (params?.scheduled_from)
56
+ searchParams.set('scheduled_from', params.scheduled_from);
57
+ if (params?.scheduled_to)
58
+ searchParams.set('scheduled_to', params.scheduled_to);
59
+ if (params?.timezone)
60
+ searchParams.set('timezone', params.timezone);
61
+ return searchParams;
62
+ }
39
63
  export class PosterlyClient {
40
64
  baseUrl;
41
65
  apiKey;
@@ -336,6 +360,8 @@ export class PosterlyClient {
336
360
  searchParams.set('status', params.status);
337
361
  if (params?.limit)
338
362
  searchParams.set('limit', String(params.limit));
363
+ if (params?.offset)
364
+ searchParams.set('offset', String(params.offset));
339
365
  const qs = searchParams.toString();
340
366
  return this.request('GET', `/suggestions${qs ? `?${qs}` : ''}`);
341
367
  }
@@ -343,28 +369,21 @@ export class PosterlyClient {
343
369
  return this.request('POST', `/suggestions/${encodeURIComponent(id)}/dismiss`, {});
344
370
  }
345
371
  async listPosts(params) {
346
- const searchParams = new URLSearchParams();
347
- if (params?.status)
348
- searchParams.set('status', params.status);
349
- if (params?.platform)
350
- searchParams.set('platform', params.platform);
351
- if (params?.account_id)
352
- searchParams.set('account_id', params.account_id);
372
+ const searchParams = postFilterParams(params);
353
373
  if (params?.limit)
354
374
  searchParams.set('limit', String(params.limit));
355
375
  if (params?.offset)
356
376
  searchParams.set('offset', String(params.offset));
357
- if (params?.workspace_id)
358
- searchParams.set('workspace_id', params.workspace_id);
359
- if (params?.approval_status)
360
- searchParams.set('approval_status', params.approval_status);
361
- if (params?.approval_waiting_on)
362
- searchParams.set('approval_waiting_on', params.approval_waiting_on);
363
- if (params?.brand_id)
364
- searchParams.set('brand_id', params.brand_id);
377
+ if (params?.order)
378
+ searchParams.set('order', params.order);
365
379
  const qs = searchParams.toString();
366
380
  return this.request('GET', `/posts${qs ? `?${qs}` : ''}`);
367
381
  }
382
+ /** GET /posts/counts: exact totals per day, status, account, and brand for a scheduled range. */
383
+ async countPosts(params) {
384
+ const qs = postFilterParams(params).toString();
385
+ return this.request('GET', `/posts/counts${qs ? `?${qs}` : ''}`);
386
+ }
368
387
  async createPost(data) {
369
388
  return this.request('POST', '/posts', data);
370
389
  }
@@ -581,6 +600,8 @@ export class PosterlyClient {
581
600
  searchParams.set('type', params.type);
582
601
  if (params?.limit)
583
602
  searchParams.set('limit', String(params.limit));
603
+ if (params?.offset)
604
+ searchParams.set('offset', String(params.offset));
584
605
  const qs = searchParams.toString();
585
606
  return this.request('GET', `/activity${qs ? `?${qs}` : ''}`);
586
607
  }
@@ -588,6 +609,8 @@ export class PosterlyClient {
588
609
  const searchParams = new URLSearchParams();
589
610
  if (params?.limit)
590
611
  searchParams.set('limit', String(params.limit));
612
+ if (params?.offset)
613
+ searchParams.set('offset', String(params.offset));
591
614
  if (params?.since)
592
615
  searchParams.set('since', params.since);
593
616
  if (params?.include_content)
@@ -661,6 +684,8 @@ export class PosterlyClient {
661
684
  const searchParams = new URLSearchParams();
662
685
  if (params?.limit)
663
686
  searchParams.set('limit', String(params.limit));
687
+ if (params?.offset)
688
+ searchParams.set('offset', String(params.offset));
664
689
  if (params?.dropSessionId)
665
690
  searchParams.set('drop_session_id', params.dropSessionId);
666
691
  const qs = searchParams.toString();
@@ -1,4 +1,4 @@
1
- import type { Account, LearnedVoice, Post } from './api-client.js';
1
+ import type { Account, LearnedVoice, ListPostsResponse, Post, PostCountsResponse, PostsRangeEcho } from './api-client.js';
2
2
  type CellValue = string | number | boolean | null | undefined;
3
3
  export declare const DASHBOARD_CALENDAR_URL: string;
4
4
  export declare const DASHBOARD_TABLE_URL: string;
@@ -41,4 +41,41 @@ export declare function compactText(value: string | null | undefined, maxLength?
41
41
  export declare function mdLearnedVoice(learned: LearnedVoice | null | undefined): string | null;
42
42
  export declare function statusLabel(status: string | null | undefined): string;
43
43
  export declare function code(value: CellValue): string;
44
- export {};
44
+ /**
45
+ * The completeness line every paged list prints, so an agent always knows
46
+ * whether it has seen everything. Shared by the npm package and the hosted
47
+ * endpoint.
48
+ */
49
+ export declare function formatPagingNote(input: {
50
+ noun: string;
51
+ tool: string;
52
+ offset?: number | null;
53
+ count: number;
54
+ total?: number | null;
55
+ hasMore: boolean;
56
+ nextOffset?: number | null;
57
+ }): string;
58
+ export type PostListRange = PostsRangeEcho;
59
+ export type PostListPage = ListPostsResponse;
60
+ export type { PostCountsResponse };
61
+ /** "Wed, Oct 1, 2026, 09:00" in `timeZone`; the raw value when it is not a date. */
62
+ export declare function formatDateTimeInZone(value: string | null | undefined, timeZone: string): string;
63
+ /** "Wed, Oct 1, 2026" for a YYYY-MM-DD calendar day. */
64
+ export declare function formatCalendarDay(day: string): string;
65
+ /**
66
+ * The scheduled range in plain words. Whole days read as "Oct 1, 2026 to
67
+ * Oct 31, 2026" (the API's `to` is exclusive, so a midnight `to` ends the
68
+ * day before); anything else reads as "from X up to Y".
69
+ */
70
+ export declare function formatPostRange(range: PostListRange): string;
71
+ /**
72
+ * One page of list_posts. Always says whether every matching post is shown,
73
+ * and when it is not, exactly which offset to call next, so an agent never
74
+ * mistakes the first page for the whole schedule.
75
+ */
76
+ export declare function formatPostListPage(data: PostListPage, options: {
77
+ timeZone: string;
78
+ dashboardUrl?: string;
79
+ }): string;
80
+ /** The counts view of list_posts: exact totals per day, status, account, and brand. */
81
+ export declare function formatPostCounts(data: PostCountsResponse): string;
@@ -229,3 +229,161 @@ function isInCurrentMonth(value) {
229
229
  const now = new Date();
230
230
  return date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth();
231
231
  }
232
+ /**
233
+ * The completeness line every paged list prints, so an agent always knows
234
+ * whether it has seen everything. Shared by the npm package and the hosted
235
+ * endpoint.
236
+ */
237
+ export function formatPagingNote(input) {
238
+ const offset = input.offset ?? 0;
239
+ const first = offset + 1;
240
+ const last = offset + input.count;
241
+ const ofTotal = typeof input.total === 'number' ? ` of ${input.total}` : '';
242
+ if (input.hasMore) {
243
+ const next = typeof input.nextOffset === 'number' ? input.nextOffset : last;
244
+ return `**Not complete: showing ${first}-${last}${ofTotal} ${input.noun}.** More available: call ${input.tool} again with the same filters and offset=${next}. Keep paging until this says complete before drawing any conclusion about totals.`;
245
+ }
246
+ if (offset === 0) {
247
+ return `**Complete: all ${typeof input.total === 'number' ? input.total : input.count} ${input.noun} are shown.**`;
248
+ }
249
+ return `**Complete: this is the last page (${input.noun} ${first}-${last}${ofTotal}).**`;
250
+ }
251
+ function zonedParts(value, timeZone) {
252
+ const date = typeof value === 'string' ? new Date(value) : value;
253
+ const parts = new Intl.DateTimeFormat('en-US', {
254
+ timeZone,
255
+ weekday: 'short',
256
+ month: 'short',
257
+ day: 'numeric',
258
+ year: 'numeric',
259
+ hour: '2-digit',
260
+ minute: '2-digit',
261
+ second: '2-digit',
262
+ hourCycle: 'h23',
263
+ }).formatToParts(date);
264
+ return Object.fromEntries(parts.map((part) => [part.type, part.value]));
265
+ }
266
+ /** "Wed, Oct 1, 2026, 09:00" in `timeZone`; the raw value when it is not a date. */
267
+ export function formatDateTimeInZone(value, timeZone) {
268
+ if (!value)
269
+ return 'not scheduled';
270
+ const date = new Date(value);
271
+ if (Number.isNaN(date.getTime()))
272
+ return value;
273
+ const p = zonedParts(date, timeZone);
274
+ return `${p.weekday}, ${p.month} ${p.day}, ${p.year}, ${p.hour}:${p.minute}`;
275
+ }
276
+ /** "Wed, Oct 1, 2026" for a YYYY-MM-DD calendar day. */
277
+ export function formatCalendarDay(day) {
278
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(day);
279
+ if (!match)
280
+ return day;
281
+ const p = zonedParts(new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]), 12)), 'UTC');
282
+ return `${p.weekday}, ${p.month} ${p.day}, ${p.year}`;
283
+ }
284
+ function isZonedMidnight(value, timeZone) {
285
+ const p = zonedParts(value, timeZone);
286
+ return p.hour === '00' && p.minute === '00' && p.second === '00';
287
+ }
288
+ function zonedDay(value, timeZone) {
289
+ const p = zonedParts(value, timeZone);
290
+ return `${p.month} ${p.day}, ${p.year}`;
291
+ }
292
+ /**
293
+ * The scheduled range in plain words. Whole days read as "Oct 1, 2026 to
294
+ * Oct 31, 2026" (the API's `to` is exclusive, so a midnight `to` ends the
295
+ * day before); anything else reads as "from X up to Y".
296
+ */
297
+ export function formatPostRange(range) {
298
+ const tz = range.timezone;
299
+ if (range.from && range.to && isZonedMidnight(range.from, tz) && isZonedMidnight(range.to, tz)) {
300
+ const lastDay = new Date(Date.parse(range.to) - 1);
301
+ return `${zonedDay(range.from, tz)} to ${zonedDay(lastDay, tz)} (${tz})`;
302
+ }
303
+ const from = range.from ? `from ${formatDateTimeInZone(range.from, tz)}` : '';
304
+ const to = range.to ? `up to ${formatDateTimeInZone(range.to, tz)}` : '';
305
+ return `${[from, to].filter(Boolean).join(' ')} (${tz})`;
306
+ }
307
+ function approvalCell(post) {
308
+ if (!post.approval || post.approval.status === 'none')
309
+ return 'none';
310
+ return post.approval.waiting_on ? `${post.approval.status} (waiting on ${post.approval.waiting_on})` : post.approval.status;
311
+ }
312
+ /**
313
+ * One page of list_posts. Always says whether every matching post is shown,
314
+ * and when it is not, exactly which offset to call next, so an agent never
315
+ * mistakes the first page for the whole schedule.
316
+ */
317
+ export function formatPostListPage(data, options) {
318
+ const posts = Array.isArray(data.posts) ? data.posts : [];
319
+ const total = typeof data.total === 'number' ? data.total : posts.length;
320
+ const offset = typeof data.offset === 'number' ? data.offset : 0;
321
+ const tz = options.timeZone;
322
+ const rangeLine = data.range ? `Scheduled ${formatPostRange(data.range)}` : '';
323
+ const orderLine = data.order === 'asc' ? 'oldest first' : 'newest first';
324
+ if (posts.length === 0) {
325
+ const detail = total > 0
326
+ ? `No posts on this page: offset ${offset} is past the last of the ${total} matching posts. Call again with a lower offset.`
327
+ : `No posts match these filters${data.range ? ` (${formatPostRange(data.range)})` : ''}. This is complete: there is nothing more to page through.`;
328
+ return [
329
+ mdTitle('No posts'),
330
+ detail,
331
+ '\n**Next step for the AI agent:** tell the user nothing matches that filter, then ask what they want to schedule next.',
332
+ ].join('\n');
333
+ }
334
+ const first = offset + 1;
335
+ const last = offset + posts.length;
336
+ const hasMore = typeof data.has_more === 'boolean' ? data.has_more : last < total;
337
+ const nextOffset = typeof data.next_offset === 'number' ? data.next_offset : last;
338
+ const completeness = hasMore
339
+ ? `**Not complete: showing ${first}-${last} of ${total} matching posts.** More available: call list_posts again with the same filters and offset=${nextOffset}. Keep paging until every post is shown before drawing any conclusion about totals, gaps, or how busy a day or client is. For "how many" questions, use view=counts.`
340
+ : first === 1
341
+ ? `**Complete: all ${total} matching posts are shown.**`
342
+ : `**Complete: this is the last page (posts ${first}-${last} of ${total}).**`;
343
+ const table = mdTable(['Status', 'Post ID', `Scheduled (${tz})`, 'Type', 'Account ID', 'Brand ID', 'Approval', 'Caption'],
344
+ // The API post row carries account and brand IDs only (no joined names);
345
+ // list_accounts and list_brands, or view=counts, resolve them.
346
+ posts.map((post) => [
347
+ statusLabel(post.status),
348
+ code(String(post.id)),
349
+ formatDateTimeInZone(post.scheduled_at, tz),
350
+ post.post_type || 'post',
351
+ post.social_account_id != null ? String(post.social_account_id) : 'n/a',
352
+ post.brand_id || 'none',
353
+ approvalCell(post),
354
+ compactText(post.content, 80) || '(empty)',
355
+ ]));
356
+ return [
357
+ mdTitle(hasMore || first > 1 ? `Posts ${first}-${last} of ${total}` : `Posts (${total})`, [rangeLine, orderLine, `times in ${tz}`].filter(Boolean).join(' · ')),
358
+ table,
359
+ completeness,
360
+ options.dashboardUrl ? `View in posterly: ${options.dashboardUrl}` : '',
361
+ ].filter(Boolean).join('\n\n');
362
+ }
363
+ /** The counts view of list_posts: exact totals per day, status, account, and brand. */
364
+ export function formatPostCounts(data) {
365
+ const range = data.range;
366
+ const days = Array.isArray(data.by_day) ? data.by_day : [];
367
+ const emptyDays = days.filter((day) => day.count === 0).map((day) => formatCalendarDay(day.date));
368
+ const sections = [
369
+ mdTitle(`Post counts: ${data.total} ${data.total === 1 ? 'post' : 'posts'}`, range ? formatPostRange(range) : undefined),
370
+ data.truncated
371
+ ? `**Partial grouping:** ${data.total} posts match, but only the first ${data.counted} were grouped below. The total is exact; narrow the range or filters for exact per-day numbers.`
372
+ : `**Complete:** every one of the ${data.total} matching posts is counted below.`,
373
+ mdSection('By day', mdTable(['Day', 'Posts'], days.map((day) => [formatCalendarDay(day.date), day.count]))),
374
+ // A truncated grouping only covers the earliest posts, so a day with a
375
+ // zero here may still have posts: never list "empty" days then.
376
+ days.length > 0 && !data.truncated ? `Days with no posts: ${emptyDays.length > 0 ? emptyDays.join('; ') : 'none'}.` : '',
377
+ data.truncated ? '_Per-day numbers above cover only the grouped posts, so a day showing 0 may still have posts. Narrow the range to check for empty days._' : '',
378
+ mdSection('By status', mdTable(['Status', 'Posts'], (data.by_status || []).map((row) => [statusLabel(row.status), row.count]))),
379
+ mdSection('By account', mdTable(['Account', 'Platform', 'Account ID', 'Posts'], (data.by_account || []).map((row) => [
380
+ row.username ? `@${row.username}` : row.account_id ? 'unknown account' : 'no account',
381
+ row.platform || 'n/a',
382
+ row.account_id ?? 'none',
383
+ row.count,
384
+ ]))),
385
+ mdSection('By brand', mdTable(['Brand', 'Brand ID', 'Posts'], (data.by_brand || []).map((row) => [row.name || (row.brand_id ? 'unknown brand' : 'no brand'), row.brand_id ?? 'none', row.count]))),
386
+ '**Next step for the AI agent:** these totals are exact for the filters above. To read the posts themselves, call list_posts with view=list and the same filters, paging with offset until every post is shown.',
387
+ ];
388
+ return sections.filter(Boolean).join('\n\n');
389
+ }
@@ -1 +1 @@
1
- export declare const POSTERLY_MCP_VERSION = "0.48.5";
1
+ export declare const POSTERLY_MCP_VERSION = "0.49.1";
@@ -5,4 +5,4 @@
5
5
  // tool set (minus the intentional pre-auth signup tools that only this stdio
6
6
  // package exposes). `npm run check:mcp-parity` enforces both the version match
7
7
  // and the tool-list match, and runs in the pre-commit hook.
8
- export const POSTERLY_MCP_VERSION = '0.48.5';
8
+ export const POSTERLY_MCP_VERSION = '0.49.1';
@@ -12,12 +12,12 @@ export declare const auditGoogleBusinessProfileTool: {
12
12
  account_id: z.ZodOptional<z.ZodNumber>;
13
13
  location_id: z.ZodOptional<z.ZodString>;
14
14
  }, "strip", z.ZodTypeAny, {
15
- workspace_id?: string | undefined;
16
15
  account_id?: number | undefined;
16
+ workspace_id?: string | undefined;
17
17
  location_id?: string | undefined;
18
18
  }, {
19
- workspace_id?: string | undefined;
20
19
  account_id?: number | undefined;
20
+ workspace_id?: string | undefined;
21
21
  location_id?: string | undefined;
22
22
  }>;
23
23
  execute(client: PosterlyClient, input: {
@@ -21,8 +21,8 @@ export declare const connectAccountTool: {
21
21
  connect_session_id: z.ZodOptional<z.ZodString>;
22
22
  }, "strip", z.ZodTypeAny, {
23
23
  platform: "telegram" | "bluesky" | "discord" | "wordpress" | "devto" | "hashnode" | "lemmy";
24
- username?: string | undefined;
25
24
  workspace_id?: string | undefined;
25
+ username?: string | undefined;
26
26
  connect_session_id?: string | undefined;
27
27
  bot_token?: string | undefined;
28
28
  chat_id?: string | undefined;
@@ -36,8 +36,8 @@ export declare const connectAccountTool: {
36
36
  password?: string | undefined;
37
37
  }, {
38
38
  platform: "telegram" | "bluesky" | "discord" | "wordpress" | "devto" | "hashnode" | "lemmy";
39
- username?: string | undefined;
40
39
  workspace_id?: string | undefined;
40
+ username?: string | undefined;
41
41
  connect_session_id?: string | undefined;
42
42
  bot_token?: string | undefined;
43
43
  chat_id?: string | undefined;