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
|
@@ -28,20 +28,20 @@ export declare const manageWebhookTool: {
|
|
|
28
28
|
}, "strip", z.ZodTypeAny, {
|
|
29
29
|
action: "update" | "delete" | "test";
|
|
30
30
|
webhook_id: string;
|
|
31
|
+
workspace_id?: string | null | undefined;
|
|
31
32
|
description?: string | null | undefined;
|
|
32
33
|
confirm?: boolean | undefined;
|
|
33
34
|
preview_id?: string | undefined;
|
|
34
|
-
workspace_id?: string | null | undefined;
|
|
35
35
|
url?: string | undefined;
|
|
36
36
|
is_active?: boolean | undefined;
|
|
37
37
|
events?: ("webhook.test" | "post.created" | "post.updated" | "post.deleted" | "post.publishing" | "post.published" | "post.failed" | "account.disconnected" | "analytics.synced" | "approval.requested" | "approval.approved" | "approval.changes_requested" | "approval.rejected" | "approval.commented")[] | undefined;
|
|
38
38
|
}, {
|
|
39
39
|
action: "update" | "delete" | "test";
|
|
40
40
|
webhook_id: string;
|
|
41
|
+
workspace_id?: string | null | undefined;
|
|
41
42
|
description?: string | null | undefined;
|
|
42
43
|
confirm?: boolean | undefined;
|
|
43
44
|
preview_id?: string | undefined;
|
|
44
|
-
workspace_id?: string | null | undefined;
|
|
45
45
|
url?: string | undefined;
|
|
46
46
|
is_active?: boolean | undefined;
|
|
47
47
|
events?: ("webhook.test" | "post.created" | "post.updated" | "post.deleted" | "post.publishing" | "post.published" | "post.failed" | "account.disconnected" | "analytics.synced" | "approval.requested" | "approval.approved" | "approval.changes_requested" | "approval.rejected" | "approval.commented")[] | undefined;
|
|
@@ -59,16 +59,16 @@ export declare const manageWebhookTool: {
|
|
|
59
59
|
}, "strip", z.ZodTypeAny, {
|
|
60
60
|
confirm: true;
|
|
61
61
|
webhook_id: string;
|
|
62
|
-
description?: string | null | undefined;
|
|
63
62
|
workspace_id?: string | null | undefined;
|
|
63
|
+
description?: string | null | undefined;
|
|
64
64
|
url?: string | undefined;
|
|
65
65
|
is_active?: boolean | undefined;
|
|
66
66
|
events?: ("webhook.test" | "post.created" | "post.updated" | "post.deleted" | "post.publishing" | "post.published" | "post.failed" | "account.disconnected" | "analytics.synced" | "approval.requested" | "approval.approved" | "approval.changes_requested" | "approval.rejected" | "approval.commented")[] | undefined;
|
|
67
67
|
}, {
|
|
68
68
|
confirm: true;
|
|
69
69
|
webhook_id: string;
|
|
70
|
-
description?: string | null | undefined;
|
|
71
70
|
workspace_id?: string | null | undefined;
|
|
71
|
+
description?: string | null | undefined;
|
|
72
72
|
url?: string | undefined;
|
|
73
73
|
is_active?: boolean | undefined;
|
|
74
74
|
events?: ("webhook.test" | "post.created" | "post.updated" | "post.deleted" | "post.publishing" | "post.published" | "post.failed" | "account.disconnected" | "analytics.synced" | "approval.requested" | "approval.approved" | "approval.changes_requested" | "approval.rejected" | "approval.commented")[] | undefined;
|
|
@@ -653,9 +653,9 @@ export declare const validatePostInputSchema: import("zod").ZodObject<{
|
|
|
653
653
|
workspace_id: import("zod").ZodOptional<import("zod").ZodString>;
|
|
654
654
|
}, "strip", import("zod").ZodTypeAny, {
|
|
655
655
|
platform?: "instagram" | "instagram-standalone" | "facebook" | "tiktok" | "twitter" | "threads" | "linkedin" | "youtube" | "pinterest" | "google_business" | "x" | "telegram" | "bluesky" | "discord" | "wordpress" | "devto" | "hashnode" | "lemmy" | "slack" | "mastodon" | "gmb" | "google-business" | "google_business_profile" | undefined;
|
|
656
|
-
username?: string | undefined;
|
|
657
|
-
workspace_id?: string | undefined;
|
|
658
656
|
account_id?: string | undefined;
|
|
657
|
+
workspace_id?: string | undefined;
|
|
658
|
+
username?: string | undefined;
|
|
659
659
|
caption?: string | undefined;
|
|
660
660
|
scheduled_at?: string | undefined;
|
|
661
661
|
media_url?: string | undefined;
|
|
@@ -854,9 +854,9 @@ export declare const validatePostInputSchema: import("zod").ZodObject<{
|
|
|
854
854
|
}, import("zod").ZodTypeAny, "passthrough"> | undefined;
|
|
855
855
|
}, {
|
|
856
856
|
platform?: "instagram" | "instagram-standalone" | "facebook" | "tiktok" | "twitter" | "threads" | "linkedin" | "youtube" | "pinterest" | "google_business" | "x" | "telegram" | "bluesky" | "discord" | "wordpress" | "devto" | "hashnode" | "lemmy" | "slack" | "mastodon" | "gmb" | "google-business" | "google_business_profile" | undefined;
|
|
857
|
-
username?: string | undefined;
|
|
858
|
-
workspace_id?: string | undefined;
|
|
859
857
|
account_id?: string | undefined;
|
|
858
|
+
workspace_id?: string | undefined;
|
|
859
|
+
username?: string | undefined;
|
|
860
860
|
caption?: string | undefined;
|
|
861
861
|
scheduled_at?: string | undefined;
|
|
862
862
|
media_url?: string | undefined;
|
|
@@ -1711,9 +1711,9 @@ export declare const validatePostTool: {
|
|
|
1711
1711
|
workspace_id: import("zod").ZodOptional<import("zod").ZodString>;
|
|
1712
1712
|
}, "strip", import("zod").ZodTypeAny, {
|
|
1713
1713
|
platform?: "instagram" | "instagram-standalone" | "facebook" | "tiktok" | "twitter" | "threads" | "linkedin" | "youtube" | "pinterest" | "google_business" | "x" | "telegram" | "bluesky" | "discord" | "wordpress" | "devto" | "hashnode" | "lemmy" | "slack" | "mastodon" | "gmb" | "google-business" | "google_business_profile" | undefined;
|
|
1714
|
-
username?: string | undefined;
|
|
1715
|
-
workspace_id?: string | undefined;
|
|
1716
1714
|
account_id?: string | undefined;
|
|
1715
|
+
workspace_id?: string | undefined;
|
|
1716
|
+
username?: string | undefined;
|
|
1717
1717
|
caption?: string | undefined;
|
|
1718
1718
|
scheduled_at?: string | undefined;
|
|
1719
1719
|
media_url?: string | undefined;
|
|
@@ -1912,9 +1912,9 @@ export declare const validatePostTool: {
|
|
|
1912
1912
|
}, import("zod").ZodTypeAny, "passthrough"> | undefined;
|
|
1913
1913
|
}, {
|
|
1914
1914
|
platform?: "instagram" | "instagram-standalone" | "facebook" | "tiktok" | "twitter" | "threads" | "linkedin" | "youtube" | "pinterest" | "google_business" | "x" | "telegram" | "bluesky" | "discord" | "wordpress" | "devto" | "hashnode" | "lemmy" | "slack" | "mastodon" | "gmb" | "google-business" | "google_business_profile" | undefined;
|
|
1915
|
-
username?: string | undefined;
|
|
1916
|
-
workspace_id?: string | undefined;
|
|
1917
1915
|
account_id?: string | undefined;
|
|
1916
|
+
workspace_id?: string | undefined;
|
|
1917
|
+
username?: string | undefined;
|
|
1918
1918
|
caption?: string | undefined;
|
|
1919
1919
|
scheduled_at?: string | undefined;
|
|
1920
1920
|
media_url?: string | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "posterly-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"mcpName": "io.github.awpthorp/posterly",
|
|
5
5
|
"description": "MCP server for posterly: schedule and publish social media posts across 18 platforms from any MCP client (Claude, ChatGPT, Cursor, Windsurf, Cline, and more)",
|
|
6
6
|
"license": "MIT",
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.awpthorp/posterly",
|
|
4
4
|
"title": "posterly",
|
|
5
5
|
"description": "Validate, schedule, publish, and analyze social content across 18 platforms with posterly.",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.49.0",
|
|
7
7
|
"websiteUrl": "https://www.poster.ly/mcp",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/awpthorp/posterly",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
{
|
|
15
15
|
"registryType": "npm",
|
|
16
16
|
"identifier": "posterly-mcp-server",
|
|
17
|
-
"version": "0.
|
|
17
|
+
"version": "0.49.0",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
|
20
20
|
},
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,8 @@ import { createApiKeyTool } from './tools/create-api-key.js';
|
|
|
24
24
|
import { deleteApiKeyTool } from './tools/delete-api-key.js';
|
|
25
25
|
import { listWorkspaceMembersTool } from './tools/list-workspace-members.js';
|
|
26
26
|
import { manageWorkspaceMemberTool } from './tools/manage-workspace-member.js';
|
|
27
|
+
import { listPublishingPausesTool } from './tools/list-publishing-pauses.js';
|
|
28
|
+
import { managePublishingPauseTool } from './tools/manage-publishing-pause.js';
|
|
27
29
|
import { getCreditsTool } from './tools/get-credits.js';
|
|
28
30
|
import { getSubscriptionTool } from './tools/get-subscription.js';
|
|
29
31
|
import { cancelSubscriptionTool } from './tools/cancel-subscription.js';
|
|
@@ -108,6 +110,8 @@ const STDIO_TOOLS: ToolModule[] = [
|
|
|
108
110
|
deleteApiKeyTool,
|
|
109
111
|
listWorkspaceMembersTool,
|
|
110
112
|
manageWorkspaceMemberTool,
|
|
113
|
+
listPublishingPausesTool,
|
|
114
|
+
managePublishingPauseTool,
|
|
111
115
|
getCreditsTool,
|
|
112
116
|
getSubscriptionTool,
|
|
113
117
|
cancelSubscriptionTool,
|
package/src/lib/api-client.ts
CHANGED
|
@@ -176,6 +176,11 @@ export interface PostSuggestion {
|
|
|
176
176
|
|
|
177
177
|
export interface PostSuggestionsResponse {
|
|
178
178
|
suggestions: PostSuggestion[];
|
|
179
|
+
total?: number;
|
|
180
|
+
limit?: number;
|
|
181
|
+
offset?: number;
|
|
182
|
+
has_more?: boolean;
|
|
183
|
+
next_offset?: number | null;
|
|
179
184
|
}
|
|
180
185
|
|
|
181
186
|
export interface DismissSuggestionResponse {
|
|
@@ -216,6 +221,63 @@ export interface Post {
|
|
|
216
221
|
approval?: ApiPostApproval;
|
|
217
222
|
}
|
|
218
223
|
|
|
224
|
+
/** Filters shared by GET /posts and GET /posts/counts. */
|
|
225
|
+
export interface ListPostsParams {
|
|
226
|
+
status?: string;
|
|
227
|
+
platform?: string;
|
|
228
|
+
account_id?: string;
|
|
229
|
+
workspace_id?: string;
|
|
230
|
+
approval_status?: string;
|
|
231
|
+
approval_waiting_on?: string;
|
|
232
|
+
brand_id?: string;
|
|
233
|
+
scheduled_from?: string;
|
|
234
|
+
scheduled_to?: string;
|
|
235
|
+
timezone?: string;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export interface PostsRangeEcho {
|
|
239
|
+
from: string | null;
|
|
240
|
+
to: string | null;
|
|
241
|
+
timezone: string;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface ListPostsResponse {
|
|
245
|
+
posts: Post[];
|
|
246
|
+
total: number;
|
|
247
|
+
limit?: number;
|
|
248
|
+
offset?: number;
|
|
249
|
+
has_more?: boolean;
|
|
250
|
+
next_offset?: number | null;
|
|
251
|
+
order?: 'asc' | 'desc';
|
|
252
|
+
range?: PostsRangeEcho | null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export interface PostCountsResponse {
|
|
256
|
+
total: number;
|
|
257
|
+
counted: number;
|
|
258
|
+
truncated: boolean;
|
|
259
|
+
range: PostsRangeEcho | null;
|
|
260
|
+
by_day: Array<{ date: string; count: number }>;
|
|
261
|
+
by_status: Array<{ status: string; count: number }>;
|
|
262
|
+
by_account: Array<{ account_id: string | null; platform: string | null; username: string | null; count: number }>;
|
|
263
|
+
by_brand: Array<{ brand_id: string | null; name: string | null; count: number }>;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function postFilterParams(params?: ListPostsParams): URLSearchParams {
|
|
267
|
+
const searchParams = new URLSearchParams();
|
|
268
|
+
if (params?.status) searchParams.set('status', params.status);
|
|
269
|
+
if (params?.platform) searchParams.set('platform', params.platform);
|
|
270
|
+
if (params?.account_id) searchParams.set('account_id', params.account_id);
|
|
271
|
+
if (params?.workspace_id) searchParams.set('workspace_id', params.workspace_id);
|
|
272
|
+
if (params?.approval_status) searchParams.set('approval_status', params.approval_status);
|
|
273
|
+
if (params?.approval_waiting_on) searchParams.set('approval_waiting_on', params.approval_waiting_on);
|
|
274
|
+
if (params?.brand_id) searchParams.set('brand_id', params.brand_id);
|
|
275
|
+
if (params?.scheduled_from) searchParams.set('scheduled_from', params.scheduled_from);
|
|
276
|
+
if (params?.scheduled_to) searchParams.set('scheduled_to', params.scheduled_to);
|
|
277
|
+
if (params?.timezone) searchParams.set('timezone', params.timezone);
|
|
278
|
+
return searchParams;
|
|
279
|
+
}
|
|
280
|
+
|
|
219
281
|
export interface InstagramPostSettings {
|
|
220
282
|
__type?: 'instagram' | 'instagram-standalone';
|
|
221
283
|
post_type?: 'post' | 'feed' | 'story' | 'reel' | 'carousel';
|
|
@@ -469,6 +531,54 @@ export type WorkspaceMemberConfirmFields = {
|
|
|
469
531
|
preview_id?: string;
|
|
470
532
|
};
|
|
471
533
|
|
|
534
|
+
export type PublishingPauseState = {
|
|
535
|
+
workspace: { id: string; name: string | null; paused: boolean; paused_at: string | null };
|
|
536
|
+
brands: Array<{ id: string; name: string | null; paused: boolean; paused_at: string }>;
|
|
537
|
+
viewer: {
|
|
538
|
+
role: string;
|
|
539
|
+
can_pause_workspace: boolean;
|
|
540
|
+
can_pause_brand: boolean;
|
|
541
|
+
can_resume: boolean;
|
|
542
|
+
can_publish_missed: boolean;
|
|
543
|
+
};
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* POST /publishing-pause. A pause without confirm: true changes nothing and
|
|
548
|
+
* answers 400 confirmation_required with a preview; the client returns that
|
|
549
|
+
* body (requires_confirmation: true) instead of throwing, so the tool can show
|
|
550
|
+
* it.
|
|
551
|
+
*/
|
|
552
|
+
export type PublishingPauseChangeResponse = {
|
|
553
|
+
ok?: boolean;
|
|
554
|
+
requires_confirmation?: boolean;
|
|
555
|
+
code?: string;
|
|
556
|
+
error?: string;
|
|
557
|
+
action?: 'pause' | 'resume';
|
|
558
|
+
scope?: 'workspace' | 'brand';
|
|
559
|
+
workspace_id?: string;
|
|
560
|
+
workspace_name?: string | null;
|
|
561
|
+
brand_id?: string | null;
|
|
562
|
+
brand_name?: string | null;
|
|
563
|
+
already_paused?: boolean;
|
|
564
|
+
scheduled_posts_to_hold?: number | null;
|
|
565
|
+
held_count?: number;
|
|
566
|
+
resumed_count?: number;
|
|
567
|
+
missed_count?: number;
|
|
568
|
+
missed_post_ids?: number[];
|
|
569
|
+
still_held_count?: number;
|
|
570
|
+
disconnected_count?: number;
|
|
571
|
+
failed_post_ids?: number[];
|
|
572
|
+
message?: string;
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
export type PublishMissedResponse = {
|
|
576
|
+
ok: boolean;
|
|
577
|
+
started: number;
|
|
578
|
+
skipped: number;
|
|
579
|
+
message?: string;
|
|
580
|
+
};
|
|
581
|
+
|
|
472
582
|
export type CreateApiKeyPayload = {
|
|
473
583
|
name?: string;
|
|
474
584
|
scopes?: ApiKeyScope[];
|
|
@@ -882,8 +992,12 @@ export interface ActivityItem {
|
|
|
882
992
|
|
|
883
993
|
export interface ActivityResponse {
|
|
884
994
|
items: ActivityItem[];
|
|
995
|
+
/** Items on this page (kept for older clients); use has_more to tell whether more exist. */
|
|
885
996
|
total: number;
|
|
886
997
|
limit: number;
|
|
998
|
+
offset?: number;
|
|
999
|
+
has_more?: boolean;
|
|
1000
|
+
next_offset?: number | null;
|
|
887
1001
|
}
|
|
888
1002
|
|
|
889
1003
|
export interface UpdateEntry {
|
|
@@ -902,6 +1016,11 @@ export interface UpdateEntry {
|
|
|
902
1016
|
|
|
903
1017
|
export interface UpdatesResponse {
|
|
904
1018
|
updates: UpdateEntry[];
|
|
1019
|
+
total?: number;
|
|
1020
|
+
limit?: number;
|
|
1021
|
+
offset?: number;
|
|
1022
|
+
has_more?: boolean;
|
|
1023
|
+
next_offset?: number | null;
|
|
905
1024
|
}
|
|
906
1025
|
|
|
907
1026
|
export interface ApiWebhook {
|
|
@@ -1290,6 +1409,7 @@ export class PosterlyClient {
|
|
|
1290
1409
|
method: string,
|
|
1291
1410
|
path: string,
|
|
1292
1411
|
body?: unknown,
|
|
1412
|
+
options?: { acceptConfirmationPreview?: boolean },
|
|
1293
1413
|
): Promise<T> {
|
|
1294
1414
|
this.requireApiKey();
|
|
1295
1415
|
|
|
@@ -1322,6 +1442,11 @@ export class PosterlyClient {
|
|
|
1322
1442
|
|
|
1323
1443
|
if (!res.ok) {
|
|
1324
1444
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
1445
|
+
// A write that needs confirm: true answers 400 with a preview; callers
|
|
1446
|
+
// that show previews get the body back instead of an error.
|
|
1447
|
+
if (options?.acceptConfirmationPreview && res.status === 400 && err?.requires_confirmation === true) {
|
|
1448
|
+
return err as T;
|
|
1449
|
+
}
|
|
1325
1450
|
throw new Error(formatApiError(res, err));
|
|
1326
1451
|
}
|
|
1327
1452
|
|
|
@@ -1411,6 +1536,30 @@ export class PosterlyClient {
|
|
|
1411
1536
|
return this.request('DELETE', `${path}${qs ? `?${qs}` : ''}`);
|
|
1412
1537
|
}
|
|
1413
1538
|
|
|
1539
|
+
async getPublishingPause(params?: { workspace_id?: string }): Promise<PublishingPauseState> {
|
|
1540
|
+
const query = new URLSearchParams();
|
|
1541
|
+
if (params?.workspace_id) query.set('workspace_id', params.workspace_id);
|
|
1542
|
+
const qs = query.toString();
|
|
1543
|
+
return this.request('GET', `/publishing-pause${qs ? `?${qs}` : ''}`);
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
async changePublishingPause(data: {
|
|
1547
|
+
action: 'pause' | 'resume';
|
|
1548
|
+
brand_id?: string;
|
|
1549
|
+
workspace_id?: string;
|
|
1550
|
+
confirm?: boolean;
|
|
1551
|
+
}): Promise<PublishingPauseChangeResponse> {
|
|
1552
|
+
return this.request('POST', '/publishing-pause', data, { acceptConfirmationPreview: true });
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
async publishMissedPausedPosts(data: {
|
|
1556
|
+
post_ids: number[];
|
|
1557
|
+
workspace_id?: string;
|
|
1558
|
+
confirm: true;
|
|
1559
|
+
}): Promise<PublishMissedResponse> {
|
|
1560
|
+
return this.request('POST', '/publishing-pause/publish-missed', data);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1414
1563
|
async getSubscription(): Promise<SubscriptionSummaryResponse> {
|
|
1415
1564
|
return this.request('GET', '/subscription');
|
|
1416
1565
|
}
|
|
@@ -1569,11 +1718,13 @@ export class PosterlyClient {
|
|
|
1569
1718
|
account_id?: string | number;
|
|
1570
1719
|
status?: 'pending' | 'scheduled' | 'dismissed';
|
|
1571
1720
|
limit?: number;
|
|
1721
|
+
offset?: number;
|
|
1572
1722
|
}): Promise<PostSuggestionsResponse> {
|
|
1573
1723
|
const searchParams = new URLSearchParams();
|
|
1574
1724
|
if (params?.account_id) searchParams.set('account_id', String(params.account_id));
|
|
1575
1725
|
if (params?.status) searchParams.set('status', params.status);
|
|
1576
1726
|
if (params?.limit) searchParams.set('limit', String(params.limit));
|
|
1727
|
+
if (params?.offset) searchParams.set('offset', String(params.offset));
|
|
1577
1728
|
const qs = searchParams.toString();
|
|
1578
1729
|
return this.request('GET', `/suggestions${qs ? `?${qs}` : ''}`);
|
|
1579
1730
|
}
|
|
@@ -1582,32 +1733,26 @@ export class PosterlyClient {
|
|
|
1582
1733
|
return this.request('POST', `/suggestions/${encodeURIComponent(id)}/dismiss`, {});
|
|
1583
1734
|
}
|
|
1584
1735
|
|
|
1585
|
-
async listPosts(params?: {
|
|
1586
|
-
status?: string;
|
|
1587
|
-
platform?: string;
|
|
1588
|
-
account_id?: string;
|
|
1736
|
+
async listPosts(params?: ListPostsParams & {
|
|
1589
1737
|
limit?: number;
|
|
1590
1738
|
offset?: number;
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
brand_id?: string;
|
|
1595
|
-
}): Promise<{ posts: Post[]; total: number }> {
|
|
1596
|
-
const searchParams = new URLSearchParams();
|
|
1597
|
-
if (params?.status) searchParams.set('status', params.status);
|
|
1598
|
-
if (params?.platform) searchParams.set('platform', params.platform);
|
|
1599
|
-
if (params?.account_id) searchParams.set('account_id', params.account_id);
|
|
1739
|
+
order?: 'asc' | 'desc';
|
|
1740
|
+
}): Promise<ListPostsResponse> {
|
|
1741
|
+
const searchParams = postFilterParams(params);
|
|
1600
1742
|
if (params?.limit) searchParams.set('limit', String(params.limit));
|
|
1601
1743
|
if (params?.offset) searchParams.set('offset', String(params.offset));
|
|
1602
|
-
if (params?.
|
|
1603
|
-
if (params?.approval_status) searchParams.set('approval_status', params.approval_status);
|
|
1604
|
-
if (params?.approval_waiting_on) searchParams.set('approval_waiting_on', params.approval_waiting_on);
|
|
1605
|
-
if (params?.brand_id) searchParams.set('brand_id', params.brand_id);
|
|
1744
|
+
if (params?.order) searchParams.set('order', params.order);
|
|
1606
1745
|
|
|
1607
1746
|
const qs = searchParams.toString();
|
|
1608
1747
|
return this.request('GET', `/posts${qs ? `?${qs}` : ''}`);
|
|
1609
1748
|
}
|
|
1610
1749
|
|
|
1750
|
+
/** GET /posts/counts: exact totals per day, status, account, and brand for a scheduled range. */
|
|
1751
|
+
async countPosts(params: ListPostsParams): Promise<PostCountsResponse> {
|
|
1752
|
+
const qs = postFilterParams(params).toString();
|
|
1753
|
+
return this.request('GET', `/posts/counts${qs ? `?${qs}` : ''}`);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1611
1756
|
async createPost(data: CreatePostPayload): Promise<CreatePostResponse> {
|
|
1612
1757
|
return this.request('POST', '/posts', data);
|
|
1613
1758
|
}
|
|
@@ -1975,6 +2120,7 @@ export class PosterlyClient {
|
|
|
1975
2120
|
since?: string;
|
|
1976
2121
|
type?: 'all' | 'activity' | 'publish';
|
|
1977
2122
|
limit?: number;
|
|
2123
|
+
offset?: number;
|
|
1978
2124
|
}): Promise<ActivityResponse> {
|
|
1979
2125
|
const searchParams = new URLSearchParams();
|
|
1980
2126
|
if (params?.workspace_id) searchParams.set('workspace_id', params.workspace_id);
|
|
@@ -1982,17 +2128,20 @@ export class PosterlyClient {
|
|
|
1982
2128
|
if (params?.since) searchParams.set('since', params.since);
|
|
1983
2129
|
if (params?.type) searchParams.set('type', params.type);
|
|
1984
2130
|
if (params?.limit) searchParams.set('limit', String(params.limit));
|
|
2131
|
+
if (params?.offset) searchParams.set('offset', String(params.offset));
|
|
1985
2132
|
const qs = searchParams.toString();
|
|
1986
2133
|
return this.request('GET', `/activity${qs ? `?${qs}` : ''}`);
|
|
1987
2134
|
}
|
|
1988
2135
|
|
|
1989
2136
|
async getUpdates(params?: {
|
|
1990
2137
|
limit?: number;
|
|
2138
|
+
offset?: number;
|
|
1991
2139
|
since?: string;
|
|
1992
2140
|
include_content?: boolean;
|
|
1993
2141
|
}): Promise<UpdatesResponse> {
|
|
1994
2142
|
const searchParams = new URLSearchParams();
|
|
1995
2143
|
if (params?.limit) searchParams.set('limit', String(params.limit));
|
|
2144
|
+
if (params?.offset) searchParams.set('offset', String(params.offset));
|
|
1996
2145
|
if (params?.since) searchParams.set('since', params.since);
|
|
1997
2146
|
if (params?.include_content) searchParams.set('include_content', 'true');
|
|
1998
2147
|
const qs = searchParams.toString();
|
|
@@ -2115,6 +2264,7 @@ export class PosterlyClient {
|
|
|
2115
2264
|
|
|
2116
2265
|
async listMedia(params?: {
|
|
2117
2266
|
limit?: number;
|
|
2267
|
+
offset?: number;
|
|
2118
2268
|
dropSessionId?: string;
|
|
2119
2269
|
}): Promise<{
|
|
2120
2270
|
media: Array<{
|
|
@@ -2128,9 +2278,14 @@ export class PosterlyClient {
|
|
|
2128
2278
|
drop_session_id: string | null;
|
|
2129
2279
|
}>;
|
|
2130
2280
|
total: number;
|
|
2281
|
+
limit?: number;
|
|
2282
|
+
offset?: number;
|
|
2283
|
+
has_more?: boolean;
|
|
2284
|
+
next_offset?: number | null;
|
|
2131
2285
|
}> {
|
|
2132
2286
|
const searchParams = new URLSearchParams();
|
|
2133
2287
|
if (params?.limit) searchParams.set('limit', String(params.limit));
|
|
2288
|
+
if (params?.offset) searchParams.set('offset', String(params.offset));
|
|
2134
2289
|
if (params?.dropSessionId) searchParams.set('drop_session_id', params.dropSessionId);
|
|
2135
2290
|
const qs = searchParams.toString();
|
|
2136
2291
|
return this.request('GET', `/media${qs ? `?${qs}` : ''}`);
|
package/src/lib/format.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
|
|
|
3
3
|
type CellValue = string | number | boolean | null | undefined;
|
|
4
4
|
|
|
@@ -244,3 +244,203 @@ function isInCurrentMonth(value?: string | null): boolean {
|
|
|
244
244
|
const now = new Date();
|
|
245
245
|
return date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth();
|
|
246
246
|
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The completeness line every paged list prints, so an agent always knows
|
|
250
|
+
* whether it has seen everything. Shared by the npm package and the hosted
|
|
251
|
+
* endpoint.
|
|
252
|
+
*/
|
|
253
|
+
export function formatPagingNote(input: {
|
|
254
|
+
noun: string;
|
|
255
|
+
tool: string;
|
|
256
|
+
offset?: number | null;
|
|
257
|
+
count: number;
|
|
258
|
+
total?: number | null;
|
|
259
|
+
hasMore: boolean;
|
|
260
|
+
nextOffset?: number | null;
|
|
261
|
+
}): string {
|
|
262
|
+
const offset = input.offset ?? 0;
|
|
263
|
+
const first = offset + 1;
|
|
264
|
+
const last = offset + input.count;
|
|
265
|
+
const ofTotal = typeof input.total === 'number' ? ` of ${input.total}` : '';
|
|
266
|
+
if (input.hasMore) {
|
|
267
|
+
const next = typeof input.nextOffset === 'number' ? input.nextOffset : last;
|
|
268
|
+
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.`;
|
|
269
|
+
}
|
|
270
|
+
if (offset === 0) {
|
|
271
|
+
return `**Complete: all ${typeof input.total === 'number' ? input.total : input.count} ${input.noun} are shown.**`;
|
|
272
|
+
}
|
|
273
|
+
return `**Complete: this is the last page (${input.noun} ${first}-${last}${ofTotal}).**`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// list_posts: shared by the npm package and the hosted endpoint
|
|
278
|
+
// (lib/mcp/http-tools.ts imports these), so both surfaces print the same
|
|
279
|
+
// completeness line and the same times for the same response.
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
export type PostListRange = PostsRangeEcho;
|
|
283
|
+
export type PostListPage = ListPostsResponse;
|
|
284
|
+
export type { PostCountsResponse };
|
|
285
|
+
|
|
286
|
+
function zonedParts(value: string | Date, timeZone: string): Record<string, string> {
|
|
287
|
+
const date = typeof value === 'string' ? new Date(value) : value;
|
|
288
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
289
|
+
timeZone,
|
|
290
|
+
weekday: 'short',
|
|
291
|
+
month: 'short',
|
|
292
|
+
day: 'numeric',
|
|
293
|
+
year: 'numeric',
|
|
294
|
+
hour: '2-digit',
|
|
295
|
+
minute: '2-digit',
|
|
296
|
+
second: '2-digit',
|
|
297
|
+
hourCycle: 'h23',
|
|
298
|
+
}).formatToParts(date);
|
|
299
|
+
return Object.fromEntries(parts.map((part) => [part.type, part.value]));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** "Wed, Oct 1, 2026, 09:00" in `timeZone`; the raw value when it is not a date. */
|
|
303
|
+
export function formatDateTimeInZone(value: string | null | undefined, timeZone: string): string {
|
|
304
|
+
if (!value) return 'not scheduled';
|
|
305
|
+
const date = new Date(value);
|
|
306
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
307
|
+
const p = zonedParts(date, timeZone);
|
|
308
|
+
return `${p.weekday}, ${p.month} ${p.day}, ${p.year}, ${p.hour}:${p.minute}`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** "Wed, Oct 1, 2026" for a YYYY-MM-DD calendar day. */
|
|
312
|
+
export function formatCalendarDay(day: string): string {
|
|
313
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(day);
|
|
314
|
+
if (!match) return day;
|
|
315
|
+
const p = zonedParts(new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]), 12)), 'UTC');
|
|
316
|
+
return `${p.weekday}, ${p.month} ${p.day}, ${p.year}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function isZonedMidnight(value: string, timeZone: string): boolean {
|
|
320
|
+
const p = zonedParts(value, timeZone);
|
|
321
|
+
return p.hour === '00' && p.minute === '00' && p.second === '00';
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function zonedDay(value: string | Date, timeZone: string): string {
|
|
325
|
+
const p = zonedParts(value, timeZone);
|
|
326
|
+
return `${p.month} ${p.day}, ${p.year}`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* The scheduled range in plain words. Whole days read as "Oct 1, 2026 to
|
|
331
|
+
* Oct 31, 2026" (the API's `to` is exclusive, so a midnight `to` ends the
|
|
332
|
+
* day before); anything else reads as "from X up to Y".
|
|
333
|
+
*/
|
|
334
|
+
export function formatPostRange(range: PostListRange): string {
|
|
335
|
+
const tz = range.timezone;
|
|
336
|
+
if (range.from && range.to && isZonedMidnight(range.from, tz) && isZonedMidnight(range.to, tz)) {
|
|
337
|
+
const lastDay = new Date(Date.parse(range.to) - 1);
|
|
338
|
+
return `${zonedDay(range.from, tz)} to ${zonedDay(lastDay, tz)} (${tz})`;
|
|
339
|
+
}
|
|
340
|
+
const from = range.from ? `from ${formatDateTimeInZone(range.from, tz)}` : '';
|
|
341
|
+
const to = range.to ? `up to ${formatDateTimeInZone(range.to, tz)}` : '';
|
|
342
|
+
return `${[from, to].filter(Boolean).join(' ')} (${tz})`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function approvalCell(post: Post): string {
|
|
346
|
+
if (!post.approval || post.approval.status === 'none') return 'none';
|
|
347
|
+
return post.approval.waiting_on ? `${post.approval.status} (waiting on ${post.approval.waiting_on})` : post.approval.status;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* One page of list_posts. Always says whether every matching post is shown,
|
|
352
|
+
* and when it is not, exactly which offset to call next, so an agent never
|
|
353
|
+
* mistakes the first page for the whole schedule.
|
|
354
|
+
*/
|
|
355
|
+
export function formatPostListPage(
|
|
356
|
+
data: PostListPage,
|
|
357
|
+
options: { timeZone: string; dashboardUrl?: string },
|
|
358
|
+
): string {
|
|
359
|
+
const posts = Array.isArray(data.posts) ? data.posts : [];
|
|
360
|
+
const total = typeof data.total === 'number' ? data.total : posts.length;
|
|
361
|
+
const offset = typeof data.offset === 'number' ? data.offset : 0;
|
|
362
|
+
const tz = options.timeZone;
|
|
363
|
+
const rangeLine = data.range ? `Scheduled ${formatPostRange(data.range)}` : '';
|
|
364
|
+
const orderLine = data.order === 'asc' ? 'oldest first' : 'newest first';
|
|
365
|
+
|
|
366
|
+
if (posts.length === 0) {
|
|
367
|
+
const detail = total > 0
|
|
368
|
+
? `No posts on this page: offset ${offset} is past the last of the ${total} matching posts. Call again with a lower offset.`
|
|
369
|
+
: `No posts match these filters${data.range ? ` (${formatPostRange(data.range)})` : ''}. This is complete: there is nothing more to page through.`;
|
|
370
|
+
return [
|
|
371
|
+
mdTitle('No posts'),
|
|
372
|
+
detail,
|
|
373
|
+
'\n**Next step for the AI agent:** tell the user nothing matches that filter, then ask what they want to schedule next.',
|
|
374
|
+
].join('\n');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const first = offset + 1;
|
|
378
|
+
const last = offset + posts.length;
|
|
379
|
+
const hasMore = typeof data.has_more === 'boolean' ? data.has_more : last < total;
|
|
380
|
+
const nextOffset = typeof data.next_offset === 'number' ? data.next_offset : last;
|
|
381
|
+
const completeness = hasMore
|
|
382
|
+
? `**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.`
|
|
383
|
+
: first === 1
|
|
384
|
+
? `**Complete: all ${total} matching posts are shown.**`
|
|
385
|
+
: `**Complete: this is the last page (posts ${first}-${last} of ${total}).**`;
|
|
386
|
+
|
|
387
|
+
const table = mdTable(
|
|
388
|
+
['Status', 'Post ID', `Scheduled (${tz})`, 'Type', 'Account ID', 'Brand ID', 'Approval', 'Caption'],
|
|
389
|
+
// The API post row carries account and brand IDs only (no joined names);
|
|
390
|
+
// list_accounts and list_brands, or view=counts, resolve them.
|
|
391
|
+
posts.map((post) => [
|
|
392
|
+
statusLabel(post.status),
|
|
393
|
+
code(String(post.id)),
|
|
394
|
+
formatDateTimeInZone(post.scheduled_at, tz),
|
|
395
|
+
post.post_type || 'post',
|
|
396
|
+
post.social_account_id != null ? String(post.social_account_id) : 'n/a',
|
|
397
|
+
post.brand_id || 'none',
|
|
398
|
+
approvalCell(post),
|
|
399
|
+
compactText(post.content, 80) || '(empty)',
|
|
400
|
+
]),
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
return [
|
|
404
|
+
mdTitle(
|
|
405
|
+
hasMore || first > 1 ? `Posts ${first}-${last} of ${total}` : `Posts (${total})`,
|
|
406
|
+
[rangeLine, orderLine, `times in ${tz}`].filter(Boolean).join(' · '),
|
|
407
|
+
),
|
|
408
|
+
table,
|
|
409
|
+
completeness,
|
|
410
|
+
options.dashboardUrl ? `View in posterly: ${options.dashboardUrl}` : '',
|
|
411
|
+
].filter(Boolean).join('\n\n');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** The counts view of list_posts: exact totals per day, status, account, and brand. */
|
|
415
|
+
export function formatPostCounts(data: PostCountsResponse): string {
|
|
416
|
+
const range = data.range;
|
|
417
|
+
const days = Array.isArray(data.by_day) ? data.by_day : [];
|
|
418
|
+
const emptyDays = days.filter((day) => day.count === 0).map((day) => formatCalendarDay(day.date));
|
|
419
|
+
const sections = [
|
|
420
|
+
mdTitle(`Post counts: ${data.total} ${data.total === 1 ? 'post' : 'posts'}`, range ? formatPostRange(range) : undefined),
|
|
421
|
+
data.truncated
|
|
422
|
+
? `**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.`
|
|
423
|
+
: `**Complete:** every one of the ${data.total} matching posts is counted below.`,
|
|
424
|
+
mdSection('By day', mdTable(['Day', 'Posts'], days.map((day) => [formatCalendarDay(day.date), day.count]))),
|
|
425
|
+
// A truncated grouping only covers the earliest posts, so a day with a
|
|
426
|
+
// zero here may still have posts: never list "empty" days then.
|
|
427
|
+
days.length > 0 && !data.truncated ? `Days with no posts: ${emptyDays.length > 0 ? emptyDays.join('; ') : 'none'}.` : '',
|
|
428
|
+
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._' : '',
|
|
429
|
+
mdSection('By status', mdTable(['Status', 'Posts'], (data.by_status || []).map((row) => [statusLabel(row.status), row.count]))),
|
|
430
|
+
mdSection('By account', mdTable(
|
|
431
|
+
['Account', 'Platform', 'Account ID', 'Posts'],
|
|
432
|
+
(data.by_account || []).map((row) => [
|
|
433
|
+
row.username ? `@${row.username}` : row.account_id ? 'unknown account' : 'no account',
|
|
434
|
+
row.platform || 'n/a',
|
|
435
|
+
row.account_id ?? 'none',
|
|
436
|
+
row.count,
|
|
437
|
+
]),
|
|
438
|
+
)),
|
|
439
|
+
mdSection('By brand', mdTable(
|
|
440
|
+
['Brand', 'Brand ID', 'Posts'],
|
|
441
|
+
(data.by_brand || []).map((row) => [row.name || (row.brand_id ? 'unknown brand' : 'no brand'), row.brand_id ?? 'none', row.count]),
|
|
442
|
+
)),
|
|
443
|
+
'**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.',
|
|
444
|
+
];
|
|
445
|
+
return sections.filter(Boolean).join('\n\n');
|
|
446
|
+
}
|
package/src/lib/version.ts
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';
|