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.
- package/README.md +18 -3
- package/dist/index.js +4 -0
- package/dist/lib/api-client.d.ts +149 -12
- package/dist/lib/api-client.js +59 -16
- package/dist/lib/format.d.ts +39 -2
- package/dist/lib/format.js +158 -0
- package/dist/lib/version.d.ts +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/tools/audit-google-business-profile.d.ts +2 -2
- package/dist/tools/connect-account.d.ts +2 -2
- package/dist/tools/create-post.d.ts +12 -12
- package/dist/tools/create-posts-batch.d.ts +8 -8
- package/dist/tools/create-webhook.d.ts +2 -2
- package/dist/tools/disconnect-account.d.ts +2 -2
- package/dist/tools/find-slot.d.ts +2 -2
- package/dist/tools/get-updates.d.ts +4 -0
- package/dist/tools/get-updates.js +13 -1
- package/dist/tools/list-accounts.d.ts +2 -2
- package/dist/tools/list-activity.d.ts +4 -0
- package/dist/tools/list-activity.js +16 -3
- package/dist/tools/list-analytics.d.ts +2 -2
- package/dist/tools/list-brands.d.ts +2 -2
- package/dist/tools/list-comments.d.ts +2 -2
- package/dist/tools/list-conversations.d.ts +2 -2
- package/dist/tools/list-google-business-media.d.ts +2 -2
- package/dist/tools/list-google-business-reviews.d.ts +4 -4
- package/dist/tools/list-media.d.ts +4 -0
- package/dist/tools/list-media.js +22 -1
- package/dist/tools/list-platforms.d.ts +4 -4
- package/dist/tools/list-post-suggestions.d.ts +4 -0
- package/dist/tools/list-post-suggestions.js +16 -4
- package/dist/tools/list-posts.d.ts +35 -17
- package/dist/tools/list-posts.js +63 -33
- package/dist/tools/list-publishing-pauses.d.ts +19 -0
- package/dist/tools/list-publishing-pauses.js +81 -0
- package/dist/tools/manage-comment.d.ts +2 -2
- package/dist/tools/manage-conversation.d.ts +4 -4
- package/dist/tools/manage-google-business-media.d.ts +8 -8
- package/dist/tools/manage-google-business-review.d.ts +10 -10
- package/dist/tools/manage-publishing-pause.d.ts +35 -0
- package/dist/tools/manage-publishing-pause.js +47 -0
- package/dist/tools/manage-webhook.d.ts +4 -4
- package/dist/tools/validate-post.d.ts +8 -8
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/index.ts +4 -0
- package/src/lib/api-client.ts +172 -17
- package/src/lib/format.ts +201 -1
- package/src/lib/version.ts +1 -1
- package/src/tools/get-updates.ts +15 -1
- package/src/tools/list-activity.ts +18 -3
- package/src/tools/list-media.ts +23 -2
- package/src/tools/list-post-suggestions.ts +17 -4
- package/src/tools/list-posts.ts +74 -45
- package/src/tools/list-publishing-pauses.ts +98 -0
- package/src/tools/manage-publishing-pause.ts +62 -0
- package/tool-annotations.json +5 -0
- 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.
|
|
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
|
|
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,
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -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
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -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 =
|
|
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?.
|
|
340
|
-
searchParams.set('
|
|
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();
|
package/dist/lib/format.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/lib/format.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/lib/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const POSTERLY_MCP_VERSION = "0.
|
|
1
|
+
export declare const POSTERLY_MCP_VERSION = "0.49.0";
|
package/dist/lib/version.js
CHANGED
|
@@ -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.
|
|
8
|
+
export const POSTERLY_MCP_VERSION = '0.49.0';
|