posterly-mcp-server 0.48.4 → 0.49.0

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 (58) hide show
  1. package/README.md +18 -3
  2. package/dist/index.js +4 -0
  3. package/dist/lib/api-client.d.ts +149 -12
  4. package/dist/lib/api-client.js +59 -16
  5. package/dist/lib/format.d.ts +39 -2
  6. package/dist/lib/format.js +158 -0
  7. package/dist/lib/version.d.ts +1 -1
  8. package/dist/lib/version.js +1 -1
  9. package/dist/tools/audit-google-business-profile.d.ts +2 -2
  10. package/dist/tools/connect-account.d.ts +2 -2
  11. package/dist/tools/create-post.d.ts +12 -12
  12. package/dist/tools/create-posts-batch.d.ts +8 -8
  13. package/dist/tools/create-webhook.d.ts +2 -2
  14. package/dist/tools/disconnect-account.d.ts +2 -2
  15. package/dist/tools/find-slot.d.ts +2 -2
  16. package/dist/tools/get-updates.d.ts +4 -0
  17. package/dist/tools/get-updates.js +13 -1
  18. package/dist/tools/list-accounts.d.ts +2 -2
  19. package/dist/tools/list-activity.d.ts +4 -0
  20. package/dist/tools/list-activity.js +16 -3
  21. package/dist/tools/list-analytics.d.ts +2 -2
  22. package/dist/tools/list-brands.d.ts +2 -2
  23. package/dist/tools/list-comments.d.ts +2 -2
  24. package/dist/tools/list-conversations.d.ts +2 -2
  25. package/dist/tools/list-google-business-media.d.ts +2 -2
  26. package/dist/tools/list-google-business-reviews.d.ts +4 -4
  27. package/dist/tools/list-media.d.ts +4 -0
  28. package/dist/tools/list-media.js +22 -1
  29. package/dist/tools/list-platforms.d.ts +4 -4
  30. package/dist/tools/list-post-suggestions.d.ts +4 -0
  31. package/dist/tools/list-post-suggestions.js +16 -4
  32. package/dist/tools/list-posts.d.ts +35 -17
  33. package/dist/tools/list-posts.js +63 -33
  34. package/dist/tools/list-publishing-pauses.d.ts +19 -0
  35. package/dist/tools/list-publishing-pauses.js +81 -0
  36. package/dist/tools/manage-comment.d.ts +2 -2
  37. package/dist/tools/manage-conversation.d.ts +4 -4
  38. package/dist/tools/manage-google-business-media.d.ts +8 -8
  39. package/dist/tools/manage-google-business-review.d.ts +10 -10
  40. package/dist/tools/manage-publishing-pause.d.ts +35 -0
  41. package/dist/tools/manage-publishing-pause.js +47 -0
  42. package/dist/tools/manage-webhook.d.ts +4 -4
  43. package/dist/tools/validate-post.d.ts +8 -8
  44. package/package.json +1 -1
  45. package/server.json +2 -2
  46. package/src/index.ts +4 -0
  47. package/src/lib/api-client.ts +172 -17
  48. package/src/lib/format.ts +201 -1
  49. package/src/lib/version.ts +1 -1
  50. package/src/tools/get-updates.ts +15 -1
  51. package/src/tools/list-activity.ts +18 -3
  52. package/src/tools/list-media.ts +23 -2
  53. package/src/tools/list-post-suggestions.ts +17 -4
  54. package/src/tools/list-posts.ts +74 -45
  55. package/src/tools/list-publishing-pauses.ts +98 -0
  56. package/src/tools/manage-publishing-pause.ts +62 -0
  57. package/tool-annotations.json +5 -0
  58. package/tool-registry.json +173 -6
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.4` exposes 62 tools covering 95 actions. The hosted HTTP endpoint exposes the same catalog minus the three signup tools: 59 tools covering 92 actions.
126
+ `posterly-mcp-server@0.49.0` 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 |
@@ -321,13 +327,14 @@ Google Business Profile reviews, media, and audits.
321
327
 
322
328
  ### Admin (`admin`)
323
329
 
324
- API keys, OAuth clients, webhooks, and workspace members.
330
+ API keys, OAuth clients, webhooks, workspace members, and the publishing pause.
325
331
 
326
332
  | Tool | What it does |
327
333
  | --- | --- |
328
334
  | `create_api_key` | Create an API key |
329
335
  | `delete_api_key` | Revoke an API key |
330
336
  | `list_workspace_members` | Workspace members |
337
+ | `list_publishing_pauses` | Publishing pause: whether the workspace or any brand is paused, and what you may do |
331
338
  | `list_oauth_clients` | OAuth clients |
332
339
  | `list_webhooks` | Webhooks |
333
340
  | `create_webhook` | Create a webhook |
@@ -340,6 +347,14 @@ API keys, OAuth clients, webhooks, and workspace members.
340
347
  | `update` | Change role or brands, or turn a teammate's API and AI access on or off (preview then confirm) |
341
348
  | `remove` | Remove a member or invite (preview then confirm) |
342
349
 
350
+ **`manage_publishing_pause`**: Pause or resume publishing
351
+
352
+ | Action | What it does |
353
+ | --- | --- |
354
+ | `pause` | Pause publishing for the workspace or one brand: call without `confirm` for a preview of what it holds, then `confirm: true` after the user agrees (publisher or higher) |
355
+ | `resume` | Resume publishing: future posts are scheduled again, posts that missed their time stay paused and are listed (admin or higher) |
356
+ | `publish_missed` | Publish missed posts now, with `confirm: true` after the user agrees (admin or higher) |
357
+
343
358
  **`manage_oauth_client`**: Manage an OAuth client
344
359
 
345
360
  | Action | What it does |
package/dist/index.js CHANGED
@@ -21,6 +21,8 @@ import { createApiKeyTool } from './tools/create-api-key.js';
21
21
  import { deleteApiKeyTool } from './tools/delete-api-key.js';
22
22
  import { listWorkspaceMembersTool } from './tools/list-workspace-members.js';
23
23
  import { manageWorkspaceMemberTool } from './tools/manage-workspace-member.js';
24
+ import { listPublishingPausesTool } from './tools/list-publishing-pauses.js';
25
+ import { managePublishingPauseTool } from './tools/manage-publishing-pause.js';
24
26
  import { getCreditsTool } from './tools/get-credits.js';
25
27
  import { getSubscriptionTool } from './tools/get-subscription.js';
26
28
  import { cancelSubscriptionTool } from './tools/cancel-subscription.js';
@@ -89,6 +91,8 @@ const STDIO_TOOLS = [
89
91
  deleteApiKeyTool,
90
92
  listWorkspaceMembersTool,
91
93
  manageWorkspaceMemberTool,
94
+ listPublishingPausesTool,
95
+ managePublishingPauseTool,
92
96
  getCreditsTool,
93
97
  getSubscriptionTool,
94
98
  cancelSubscriptionTool,
@@ -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';
@@ -425,6 +483,61 @@ export type WorkspaceMemberConfirmFields = {
425
483
  confirm?: boolean;
426
484
  preview_id?: string;
427
485
  };
486
+ export type PublishingPauseState = {
487
+ workspace: {
488
+ id: string;
489
+ name: string | null;
490
+ paused: boolean;
491
+ paused_at: string | null;
492
+ };
493
+ brands: Array<{
494
+ id: string;
495
+ name: string | null;
496
+ paused: boolean;
497
+ paused_at: string;
498
+ }>;
499
+ viewer: {
500
+ role: string;
501
+ can_pause_workspace: boolean;
502
+ can_pause_brand: boolean;
503
+ can_resume: boolean;
504
+ can_publish_missed: boolean;
505
+ };
506
+ };
507
+ /**
508
+ * POST /publishing-pause. A pause without confirm: true changes nothing and
509
+ * answers 400 confirmation_required with a preview; the client returns that
510
+ * body (requires_confirmation: true) instead of throwing, so the tool can show
511
+ * it.
512
+ */
513
+ export type PublishingPauseChangeResponse = {
514
+ ok?: boolean;
515
+ requires_confirmation?: boolean;
516
+ code?: string;
517
+ error?: string;
518
+ action?: 'pause' | 'resume';
519
+ scope?: 'workspace' | 'brand';
520
+ workspace_id?: string;
521
+ workspace_name?: string | null;
522
+ brand_id?: string | null;
523
+ brand_name?: string | null;
524
+ already_paused?: boolean;
525
+ scheduled_posts_to_hold?: number | null;
526
+ held_count?: number;
527
+ resumed_count?: number;
528
+ missed_count?: number;
529
+ missed_post_ids?: number[];
530
+ still_held_count?: number;
531
+ disconnected_count?: number;
532
+ failed_post_ids?: number[];
533
+ message?: string;
534
+ };
535
+ export type PublishMissedResponse = {
536
+ ok: boolean;
537
+ started: number;
538
+ skipped: number;
539
+ message?: string;
540
+ };
428
541
  export type CreateApiKeyPayload = {
429
542
  name?: string;
430
543
  scopes?: ApiKeyScope[];
@@ -836,8 +949,12 @@ export interface ActivityItem {
836
949
  }
837
950
  export interface ActivityResponse {
838
951
  items: ActivityItem[];
952
+ /** Items on this page (kept for older clients); use has_more to tell whether more exist. */
839
953
  total: number;
840
954
  limit: number;
955
+ offset?: number;
956
+ has_more?: boolean;
957
+ next_offset?: number | null;
841
958
  }
842
959
  export interface UpdateEntry {
843
960
  slug: string;
@@ -854,6 +971,11 @@ export interface UpdateEntry {
854
971
  }
855
972
  export interface UpdatesResponse {
856
973
  updates: UpdateEntry[];
974
+ total?: number;
975
+ limit?: number;
976
+ offset?: number;
977
+ has_more?: boolean;
978
+ next_offset?: number | null;
857
979
  }
858
980
  export interface ApiWebhook {
859
981
  id: string;
@@ -1159,6 +1281,20 @@ export declare class PosterlyClient {
1159
1281
  member_id?: string;
1160
1282
  invite_id?: string;
1161
1283
  }, data: WorkspaceMemberConfirmFields): Promise<WorkspaceMemberChangeResponse>;
1284
+ getPublishingPause(params?: {
1285
+ workspace_id?: string;
1286
+ }): Promise<PublishingPauseState>;
1287
+ changePublishingPause(data: {
1288
+ action: 'pause' | 'resume';
1289
+ brand_id?: string;
1290
+ workspace_id?: string;
1291
+ confirm?: boolean;
1292
+ }): Promise<PublishingPauseChangeResponse>;
1293
+ publishMissedPausedPosts(data: {
1294
+ post_ids: number[];
1295
+ workspace_id?: string;
1296
+ confirm: true;
1297
+ }): Promise<PublishMissedResponse>;
1162
1298
  getSubscription(): Promise<SubscriptionSummaryResponse>;
1163
1299
  getCredits(): Promise<CreditsSummaryResponse>;
1164
1300
  cancelSubscription(data: CancelSubscriptionPayload): Promise<CancelSubscriptionResponse>;
@@ -1247,22 +1383,16 @@ export declare class PosterlyClient {
1247
1383
  account_id?: string | number;
1248
1384
  status?: 'pending' | 'scheduled' | 'dismissed';
1249
1385
  limit?: number;
1386
+ offset?: number;
1250
1387
  }): Promise<PostSuggestionsResponse>;
1251
1388
  dismissSuggestion(id: string): Promise<DismissSuggestionResponse>;
1252
- listPosts(params?: {
1253
- status?: string;
1254
- platform?: string;
1255
- account_id?: string;
1389
+ listPosts(params?: ListPostsParams & {
1256
1390
  limit?: number;
1257
1391
  offset?: number;
1258
- workspace_id?: string;
1259
- approval_status?: string;
1260
- approval_waiting_on?: string;
1261
- brand_id?: string;
1262
- }): Promise<{
1263
- posts: Post[];
1264
- total: number;
1265
- }>;
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>;
1266
1396
  createPost(data: CreatePostPayload): Promise<CreatePostResponse>;
1267
1397
  validatePost(data: CreatePostPayload): Promise<ValidatePostResponse>;
1268
1398
  submitAgentFeedback(data: AgentFeedbackPayload): Promise<AgentFeedbackResponse>;
@@ -1474,9 +1604,11 @@ export declare class PosterlyClient {
1474
1604
  since?: string;
1475
1605
  type?: 'all' | 'activity' | 'publish';
1476
1606
  limit?: number;
1607
+ offset?: number;
1477
1608
  }): Promise<ActivityResponse>;
1478
1609
  getUpdates(params?: {
1479
1610
  limit?: number;
1611
+ offset?: number;
1480
1612
  since?: string;
1481
1613
  include_content?: boolean;
1482
1614
  }): Promise<UpdatesResponse>;
@@ -1555,6 +1687,7 @@ export declare class PosterlyClient {
1555
1687
  }>;
1556
1688
  listMedia(params?: {
1557
1689
  limit?: number;
1690
+ offset?: number;
1558
1691
  dropSessionId?: string;
1559
1692
  }): Promise<{
1560
1693
  media: Array<{
@@ -1568,6 +1701,10 @@ export declare class PosterlyClient {
1568
1701
  drop_session_id: string | null;
1569
1702
  }>;
1570
1703
  total: number;
1704
+ limit?: number;
1705
+ offset?: number;
1706
+ has_more?: boolean;
1707
+ next_offset?: number | null;
1571
1708
  }>;
1572
1709
  uploadMedia(input: {
1573
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;
@@ -116,7 +140,7 @@ export class PosterlyClient {
116
140
  throw new Error('posterly access is not installed yet. Use start_signup to begin paid setup, or set POSTERLY_API_KEY after signup.');
117
141
  }
118
142
  }
119
- async request(method, path, body) {
143
+ async request(method, path, body, options) {
120
144
  this.requireApiKey();
121
145
  const url = `${this.baseUrl}/api/v1${path}`;
122
146
  const headers = {
@@ -144,6 +168,11 @@ export class PosterlyClient {
144
168
  const res = await fetch(url, init);
145
169
  if (!res.ok) {
146
170
  const err = await res.json().catch(() => ({ error: res.statusText }));
171
+ // A write that needs confirm: true answers 400 with a preview; callers
172
+ // that show previews get the body back instead of an error.
173
+ if (options?.acceptConfirmationPreview && res.status === 400 && err?.requires_confirmation === true) {
174
+ return err;
175
+ }
147
176
  throw new Error(formatApiError(res, err));
148
177
  }
149
178
  return res.json();
@@ -204,6 +233,19 @@ export class PosterlyClient {
204
233
  const qs = params.toString();
205
234
  return this.request('DELETE', `${path}${qs ? `?${qs}` : ''}`);
206
235
  }
236
+ async getPublishingPause(params) {
237
+ const query = new URLSearchParams();
238
+ if (params?.workspace_id)
239
+ query.set('workspace_id', params.workspace_id);
240
+ const qs = query.toString();
241
+ return this.request('GET', `/publishing-pause${qs ? `?${qs}` : ''}`);
242
+ }
243
+ async changePublishingPause(data) {
244
+ return this.request('POST', '/publishing-pause', data, { acceptConfirmationPreview: true });
245
+ }
246
+ async publishMissedPausedPosts(data) {
247
+ return this.request('POST', '/publishing-pause/publish-missed', data);
248
+ }
207
249
  async getSubscription() {
208
250
  return this.request('GET', '/subscription');
209
251
  }
@@ -318,6 +360,8 @@ export class PosterlyClient {
318
360
  searchParams.set('status', params.status);
319
361
  if (params?.limit)
320
362
  searchParams.set('limit', String(params.limit));
363
+ if (params?.offset)
364
+ searchParams.set('offset', String(params.offset));
321
365
  const qs = searchParams.toString();
322
366
  return this.request('GET', `/suggestions${qs ? `?${qs}` : ''}`);
323
367
  }
@@ -325,28 +369,21 @@ export class PosterlyClient {
325
369
  return this.request('POST', `/suggestions/${encodeURIComponent(id)}/dismiss`, {});
326
370
  }
327
371
  async listPosts(params) {
328
- const searchParams = new URLSearchParams();
329
- if (params?.status)
330
- searchParams.set('status', params.status);
331
- if (params?.platform)
332
- searchParams.set('platform', params.platform);
333
- if (params?.account_id)
334
- searchParams.set('account_id', params.account_id);
372
+ const searchParams = postFilterParams(params);
335
373
  if (params?.limit)
336
374
  searchParams.set('limit', String(params.limit));
337
375
  if (params?.offset)
338
376
  searchParams.set('offset', String(params.offset));
339
- if (params?.workspace_id)
340
- searchParams.set('workspace_id', params.workspace_id);
341
- if (params?.approval_status)
342
- searchParams.set('approval_status', params.approval_status);
343
- if (params?.approval_waiting_on)
344
- searchParams.set('approval_waiting_on', params.approval_waiting_on);
345
- if (params?.brand_id)
346
- searchParams.set('brand_id', params.brand_id);
377
+ if (params?.order)
378
+ searchParams.set('order', params.order);
347
379
  const qs = searchParams.toString();
348
380
  return this.request('GET', `/posts${qs ? `?${qs}` : ''}`);
349
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
+ }
350
387
  async createPost(data) {
351
388
  return this.request('POST', '/posts', data);
352
389
  }
@@ -563,6 +600,8 @@ export class PosterlyClient {
563
600
  searchParams.set('type', params.type);
564
601
  if (params?.limit)
565
602
  searchParams.set('limit', String(params.limit));
603
+ if (params?.offset)
604
+ searchParams.set('offset', String(params.offset));
566
605
  const qs = searchParams.toString();
567
606
  return this.request('GET', `/activity${qs ? `?${qs}` : ''}`);
568
607
  }
@@ -570,6 +609,8 @@ export class PosterlyClient {
570
609
  const searchParams = new URLSearchParams();
571
610
  if (params?.limit)
572
611
  searchParams.set('limit', String(params.limit));
612
+ if (params?.offset)
613
+ searchParams.set('offset', String(params.offset));
573
614
  if (params?.since)
574
615
  searchParams.set('since', params.since);
575
616
  if (params?.include_content)
@@ -643,6 +684,8 @@ export class PosterlyClient {
643
684
  const searchParams = new URLSearchParams();
644
685
  if (params?.limit)
645
686
  searchParams.set('limit', String(params.limit));
687
+ if (params?.offset)
688
+ searchParams.set('offset', String(params.offset));
646
689
  if (params?.dropSessionId)
647
690
  searchParams.set('drop_session_id', params.dropSessionId);
648
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.4";
1
+ export declare const POSTERLY_MCP_VERSION = "0.49.0";
@@ -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.4';
8
+ export const POSTERLY_MCP_VERSION = '0.49.0';