posterly-mcp-server 0.46.0 → 0.47.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/lib/api-client.d.ts +28 -1
- package/dist/lib/api-client.js +12 -2
- package/dist/lib/version.d.ts +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/tools/create-post.d.ts +882 -6
- package/dist/tools/create-post.js +49 -1
- package/dist/tools/create-posts-batch.d.ts +318 -4
- package/dist/tools/delete-post.d.ts +4 -0
- package/dist/tools/delete-post.js +10 -5
- package/dist/tools/list-analytics.js +4 -1
- package/dist/tools/list-posts.d.ts +4 -0
- package/dist/tools/list-posts.js +5 -1
- package/dist/tools/update-post.d.ts +414 -2
- package/dist/tools/update-post.js +63 -1
- package/dist/tools/validate-post.d.ts +480 -4
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/lib/api-client.ts +41 -2
- package/src/lib/version.ts +1 -1
- package/src/tools/create-post.ts +47 -1
- package/src/tools/delete-post.ts +16 -7
- package/src/tools/list-analytics.ts +4 -1
- package/src/tools/list-posts.ts +6 -1
- package/src/tools/update-post.ts +82 -1
- package/tool-registry.json +40 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "posterly-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.1",
|
|
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.47.1",
|
|
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.47.1",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
|
20
20
|
},
|
package/src/lib/api-client.ts
CHANGED
|
@@ -190,6 +190,7 @@ export interface ApiPostApproval {
|
|
|
190
190
|
asset_status: ApiPostApprovalStatus;
|
|
191
191
|
caption_status: ApiPostApprovalStatus;
|
|
192
192
|
waiting_on: 'client' | 'team' | null;
|
|
193
|
+
pending_client_approval: boolean;
|
|
193
194
|
requested_at: string | null;
|
|
194
195
|
completed_at: string | null;
|
|
195
196
|
}
|
|
@@ -227,6 +228,23 @@ export interface InstagramPostSettings {
|
|
|
227
228
|
media_accessibility?: Array<{ index: number; asset_id?: string; source_url?: string; alt_text: string }>;
|
|
228
229
|
reel_cover_url?: string;
|
|
229
230
|
reel_thumb_offset?: number;
|
|
231
|
+
is_ai_generated?: boolean;
|
|
232
|
+
audio_id?: string;
|
|
233
|
+
caption_addon?: {
|
|
234
|
+
type?: 'none' | 'poll' | 'comment_prompt';
|
|
235
|
+
kind?: 'none' | 'poll' | 'comment_prompt';
|
|
236
|
+
options?: string[];
|
|
237
|
+
extended_duration?: boolean;
|
|
238
|
+
} | null;
|
|
239
|
+
poll_attachment?: {
|
|
240
|
+
option_a?: string;
|
|
241
|
+
option_b?: string;
|
|
242
|
+
option_c?: string;
|
|
243
|
+
option_d?: string;
|
|
244
|
+
options?: string[];
|
|
245
|
+
};
|
|
246
|
+
is_comment_prompt_used?: boolean;
|
|
247
|
+
is_poll_duration_extended?: boolean;
|
|
230
248
|
}
|
|
231
249
|
|
|
232
250
|
export type PlatformPostSettings = Record<string, unknown>;
|
|
@@ -1544,6 +1562,7 @@ export class PosterlyClient {
|
|
|
1544
1562
|
offset?: number;
|
|
1545
1563
|
workspace_id?: string;
|
|
1546
1564
|
approval_status?: string;
|
|
1565
|
+
approval_waiting_on?: string;
|
|
1547
1566
|
brand_id?: string;
|
|
1548
1567
|
}): Promise<{ posts: Post[]; total: number }> {
|
|
1549
1568
|
const searchParams = new URLSearchParams();
|
|
@@ -1554,6 +1573,7 @@ export class PosterlyClient {
|
|
|
1554
1573
|
if (params?.offset) searchParams.set('offset', String(params.offset));
|
|
1555
1574
|
if (params?.workspace_id) searchParams.set('workspace_id', params.workspace_id);
|
|
1556
1575
|
if (params?.approval_status) searchParams.set('approval_status', params.approval_status);
|
|
1576
|
+
if (params?.approval_waiting_on) searchParams.set('approval_waiting_on', params.approval_waiting_on);
|
|
1557
1577
|
if (params?.brand_id) searchParams.set('brand_id', params.brand_id);
|
|
1558
1578
|
|
|
1559
1579
|
const qs = searchParams.toString();
|
|
@@ -1614,8 +1634,27 @@ export class PosterlyClient {
|
|
|
1614
1634
|
return this.request('PATCH', `/posts/${id}`, data);
|
|
1615
1635
|
}
|
|
1616
1636
|
|
|
1617
|
-
async deletePost(
|
|
1618
|
-
|
|
1637
|
+
async deletePost(
|
|
1638
|
+
id: number,
|
|
1639
|
+
options?: { confirmPublished?: boolean },
|
|
1640
|
+
): Promise<{ deleted: boolean; id: number; platform_delete?: string }> {
|
|
1641
|
+
const qs = options?.confirmPublished ? '?confirm_published=true' : '';
|
|
1642
|
+
return this.request('DELETE', `/posts/${id}${qs}`);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
async setPostApproval(
|
|
1646
|
+
id: number,
|
|
1647
|
+
data: {
|
|
1648
|
+
action: 'request' | 'approve' | 'request_changes' | 'reject';
|
|
1649
|
+
axis?: 'asset' | 'caption';
|
|
1650
|
+
note?: string;
|
|
1651
|
+
},
|
|
1652
|
+
): Promise<Record<string, unknown>> {
|
|
1653
|
+
return this.request('POST', `/posts/${id}/approval`, {
|
|
1654
|
+
action: data.action,
|
|
1655
|
+
...(data.axis ? { axis: data.axis } : {}),
|
|
1656
|
+
...(data.note ? { note: data.note } : {}),
|
|
1657
|
+
});
|
|
1619
1658
|
}
|
|
1620
1659
|
|
|
1621
1660
|
async updatePostStatus(
|
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.47.1';
|
package/src/tools/create-post.ts
CHANGED
|
@@ -31,6 +31,31 @@ export const instagramSettingsSchema = z.object({
|
|
|
31
31
|
reel_thumb_offset: z.number().nonnegative().optional(),
|
|
32
32
|
is_ai_generated: z.boolean().optional().describe('Label the parent Instagram media container as AI-generated. For carousels, set on the parent only.'),
|
|
33
33
|
audio_id: z.string().optional().describe('Licensed Instagram audio ID for Reels. Meta-linked Facebook Login accounts only.'),
|
|
34
|
+
caption_addon: z.union([
|
|
35
|
+
z.object({
|
|
36
|
+
type: z.enum(['none', 'poll', 'comment_prompt']).optional(),
|
|
37
|
+
kind: z.enum(['none', 'poll', 'comment_prompt']).optional(),
|
|
38
|
+
options: z.array(z.string()).max(4).optional(),
|
|
39
|
+
extended_duration: z.boolean().optional(),
|
|
40
|
+
poll_attachment: z.object({
|
|
41
|
+
option_a: z.string().optional(),
|
|
42
|
+
option_b: z.string().optional(),
|
|
43
|
+
option_c: z.string().optional(),
|
|
44
|
+
option_d: z.string().optional(),
|
|
45
|
+
options: z.array(z.string()).max(4).optional(),
|
|
46
|
+
}).optional(),
|
|
47
|
+
}),
|
|
48
|
+
z.null(),
|
|
49
|
+
]).optional().describe('Optional Instagram caption add-on. type is none, poll, or comment_prompt. A poll needs 2 to 4 options of 1 to 25 characters (an emoji counts as one) and the caption is the question. A comment prompt uses the caption as the prompt. Facebook Login only. Not available on Stories. At most one add-on per post. Pass null or type none to clear it.'),
|
|
50
|
+
poll_attachment: z.object({
|
|
51
|
+
option_a: z.string().optional(),
|
|
52
|
+
option_b: z.string().optional(),
|
|
53
|
+
option_c: z.string().optional(),
|
|
54
|
+
option_d: z.string().optional(),
|
|
55
|
+
options: z.array(z.string()).max(4).optional(),
|
|
56
|
+
}).optional().describe('Instagram poll choices. option_a and option_b are required, option_c and option_d are optional, and option_d requires option_c. Mutually exclusive with a comment prompt. The caption is the question. Serialised JSON must be 200 characters or fewer.'),
|
|
57
|
+
is_comment_prompt_used: z.boolean().optional().describe('Set true to attach an Instagram comment prompt. The caption is the prompt. Mutually exclusive with a poll. Facebook Login only. Not available on Stories.'),
|
|
58
|
+
is_poll_duration_extended: z.boolean().optional().describe('Set true only together with a poll, to request the longer Instagram poll duration.'),
|
|
34
59
|
});
|
|
35
60
|
|
|
36
61
|
export const platformSettingsSchema = z.object({
|
|
@@ -124,6 +149,17 @@ export const platformSettingsSchema = z.object({
|
|
|
124
149
|
paid_partnership: z.boolean().optional(), // X paid partnership
|
|
125
150
|
is_ai_generated: z.boolean().optional(),
|
|
126
151
|
audio_id: z.string().optional(),
|
|
152
|
+
caption_addon: z.union([
|
|
153
|
+
z.object({
|
|
154
|
+
type: z.enum(['none', 'poll', 'comment_prompt']).optional(),
|
|
155
|
+
options: z.array(z.string()).max(4).optional(),
|
|
156
|
+
extended_duration: z.boolean().optional(),
|
|
157
|
+
}),
|
|
158
|
+
z.null(),
|
|
159
|
+
]).optional(),
|
|
160
|
+
poll_attachment: z.record(z.string(), z.unknown()).optional(),
|
|
161
|
+
is_comment_prompt_used: z.boolean().optional(),
|
|
162
|
+
is_poll_duration_extended: z.boolean().optional(),
|
|
127
163
|
// tags (z.array of strings) also serve as Bluesky hidden discovery tags
|
|
128
164
|
// AI-content disclosure (TikTok is_aigc, X made_with_ai, YouTube containsSyntheticMedia, Pinterest ai_disclosures)
|
|
129
165
|
ai_generated: z.boolean().optional(),
|
|
@@ -314,7 +350,7 @@ export const createPostTool = {
|
|
|
314
350
|
'Provide either account_id OR username+platform to identify the account. If scheduled_at is omitted, the post publishes immediately. If workspace_id is omitted, the server resolves one from the social account, falling back to the caller\'s default (personal) workspace - pass workspace_id explicitly if the user has more than one workspace.\n\n' +
|
|
315
351
|
'MEDIA URLS: Posterly storage URLs are used as-is. Third-party HTTP(S) media URLs are copied into posterly storage before the post is saved, so API-created posts do not later fail on expired signed URLs. Short-lived signed URLs must still be live when this tool runs. You can also call `upload_media` with `url` first if you want to upload once and reuse the returned posterly URL across posts.\n\n' +
|
|
316
352
|
'THREADS: Pass `thread_posts` (an array of 2+ strings) to schedule a multi-post thread on X (Twitter) or Threads (Meta). The first entry is the lead post; the rest are published as replies in the same chain. X entries are capped at 280 characters each (4000 for verified, 25000 for organization accounts); Threads entries are capped at 500 characters each. When `thread_posts` is set, `caption` is ignored.\n\n' +
|
|
317
|
-
'PLATFORM SETTINGS: Pass `platform_settings` for composer-equivalent controls: Facebook story/reel/backgrounds, YouTube title/privacy/thumbnail/brandPartner, LinkedIn document titles/mentions/polls/content_call_to_action_label (including BUY_NOW and SHOP_NOW), TikTok direct-post privacy/toggles/commercial disclosure, Pinterest board/title/link/product_tags, GBP event/offer/CTA/recurrence, X reply settings/polls/paid_partnership, Threads reply controls/text attachments/ghost posts/spoilers/reply approvals/GIPHY, Telegram polls/buttons/video cover/live photo, Slack markdown/blocks, Mastodon visibility/quote/poll (media+poll allowed), and Instagram feed/story/reel/carousel/native media tags/collaborators/first comments/trial Reels/is_ai_generated/audio_id. For Instagram, @username in caption is only a caption mention; `user_tags` creates native media tags and `collaborators` sends co-author invitations only when the selected Meta-linked account reports those capabilities. Call `list_platforms` with `view: "schema"` and the account ID first. `instagram_settings` remains as a backwards-compatible alias.\n\n' +
|
|
353
|
+
'PLATFORM SETTINGS: Pass `platform_settings` for composer-equivalent controls: Facebook story/reel/backgrounds, YouTube title/privacy/thumbnail/brandPartner, LinkedIn document titles/mentions/polls/content_call_to_action_label (including BUY_NOW and SHOP_NOW), TikTok direct-post privacy/toggles/commercial disclosure, Pinterest board/title/link/product_tags, GBP event/offer/CTA/recurrence, X reply settings/polls/paid_partnership, Threads reply controls/text attachments/ghost posts/spoilers/reply approvals/GIPHY, Telegram polls/buttons/video cover/live photo, Slack markdown/blocks, Mastodon visibility/quote/poll (media+poll allowed), and Instagram feed/story/reel/carousel/native media tags/collaborators/first comments/trial Reels/is_ai_generated/audio_id/caption add-ons. Instagram caption add-ons (`caption_addon`, or `poll_attachment` / `is_comment_prompt_used`) need a Facebook Login account, a non-empty caption, and a feed photo, Reel, or carousel parent. Stories are rejected. A poll and a comment prompt cannot be combined. For Instagram, @username in caption is only a caption mention; `user_tags` creates native media tags and `collaborators` sends co-author invitations only when the selected Meta-linked account reports those capabilities. Call `list_platforms` with `view: "schema"` and the account ID first. `instagram_settings` remains as a backwards-compatible alias.\n\n' +
|
|
318
354
|
'TIKTOK MEDIA: TikTok supports a single video or a photo slideshow of 1 to 35 images, never multiple videos. Image media auto-detects as a slideshow, so you usually do not set post_type or media_type for photos and every image is posted. Pass `platform_settings.media_type: "PHOTO"` to force a slideshow explicitly.',
|
|
319
355
|
inputSchema: createPostInputSchema,
|
|
320
356
|
|
|
@@ -363,6 +399,16 @@ export const createPostTool = {
|
|
|
363
399
|
if (instagram_settings?.first_comment) {
|
|
364
400
|
lines.push('• Instagram first comment: yes');
|
|
365
401
|
}
|
|
402
|
+
const captionAddonType = instagram_settings?.caption_addon && typeof instagram_settings.caption_addon === 'object'
|
|
403
|
+
? instagram_settings.caption_addon.type || instagram_settings.caption_addon.kind
|
|
404
|
+
: null;
|
|
405
|
+
if (captionAddonType && captionAddonType !== 'none') {
|
|
406
|
+
lines.push(`• Instagram caption add-on: ${captionAddonType}`);
|
|
407
|
+
} else if (instagram_settings?.is_comment_prompt_used) {
|
|
408
|
+
lines.push('• Instagram caption add-on: comment_prompt');
|
|
409
|
+
} else if (instagram_settings?.poll_attachment) {
|
|
410
|
+
lines.push('• Instagram caption add-on: poll');
|
|
411
|
+
}
|
|
366
412
|
if (platform_settings) {
|
|
367
413
|
lines.push('• Platform settings: yes');
|
|
368
414
|
}
|
package/src/tools/delete-post.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { previewThenConfirm, validateArgs, type ToolContext } from '../lib/tool-
|
|
|
6
6
|
type DeletePostInput = {
|
|
7
7
|
post_id?: number;
|
|
8
8
|
group_id?: string;
|
|
9
|
+
confirm_published?: boolean;
|
|
9
10
|
confirm?: boolean;
|
|
10
11
|
preview_id?: string;
|
|
11
12
|
};
|
|
@@ -13,10 +14,14 @@ type DeletePostInput = {
|
|
|
13
14
|
export const deletePostTool = {
|
|
14
15
|
name: 'delete_post',
|
|
15
16
|
description:
|
|
16
|
-
'Delete posts. DESTRUCTIVE and IRREVERSIBLE: deleted posts and captions cannot be recovered
|
|
17
|
+
'Delete posts. DESTRUCTIVE and IRREVERSIBLE: deleted posts and captions cannot be recovered. post_id: delete one post; fetch it with `get_post` first, show the user what will be deleted (caption, account, scheduled time), and pass confirm=true only after explicit confirmation ("yes delete it", "remove it"). A post that is currently publishing is never deleted. A published post is taken down only when the user also explicitly confirms the live removal and you pass confirm_published=true. Instagram and Facebook are attempted when the stored id and token allow it. Every other platform, or a live URL with no id, returns platform_delete_unsupported and the posterly row is kept. A published post with no live URL and no platform id is removed from posterly only. group_id: delete every draft, scheduled, failed, or paused post matching a caller-defined group_id, post_group_id, api_group_id, or release_id. A group delete never removes published posts. Preview then confirm: call without confirm to get a preview and preview_id, show it, then call again with confirm=true and that preview_id. Never delete several posts one by one without listing each and confirming the full list.',
|
|
17
18
|
inputSchema: z.object({
|
|
18
19
|
post_id: z.number().optional().describe('Delete this one post.'),
|
|
19
|
-
group_id: z.string().min(1).max(160).optional().describe('Delete every unpublished post in this group or release instead.'),
|
|
20
|
+
group_id: z.string().min(1).max(160).optional().describe('Delete every unpublished post in this group or release instead. Never deletes published posts.'),
|
|
21
|
+
confirm_published: z
|
|
22
|
+
.boolean()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe('With post_id only. True only after the user explicitly confirms taking a live Instagram or Facebook post down. Without it, a published post is refused and kept.'),
|
|
20
25
|
confirm: z
|
|
21
26
|
.boolean()
|
|
22
27
|
.optional()
|
|
@@ -44,7 +49,7 @@ export const deletePostTool = {
|
|
|
44
49
|
// GET /api/v1/posts has no group filter, so the preview says in plain
|
|
45
50
|
// words which posts match instead of listing them.
|
|
46
51
|
describe: async () => [
|
|
47
|
-
`Deletes every draft, scheduled, failed, or paused post whose group_id, post_group_id, api_group_id, or release_id is ${code(args.group_id)}.
|
|
52
|
+
`Deletes every draft, scheduled, failed, or paused post whose group_id, post_group_id, api_group_id, or release_id is ${code(args.group_id)}. A group delete never removes published posts. This cannot be undone.`,
|
|
48
53
|
'To see the posts first, call list_posts and check each post\'s group or release ID.',
|
|
49
54
|
].join('\n\n'),
|
|
50
55
|
run: async () => {
|
|
@@ -53,17 +58,21 @@ export const deletePostTool = {
|
|
|
53
58
|
},
|
|
54
59
|
});
|
|
55
60
|
}
|
|
56
|
-
return deleteOne(client, args as { post_id: number; confirm?: boolean });
|
|
61
|
+
return deleteOne(client, args as { post_id: number; confirm?: boolean; confirm_published?: boolean });
|
|
57
62
|
},
|
|
58
63
|
};
|
|
59
64
|
|
|
60
|
-
async function deleteOne(
|
|
65
|
+
async function deleteOne(
|
|
66
|
+
client: PosterlyClient,
|
|
67
|
+
input: { post_id: number; confirm?: boolean; confirm_published?: boolean },
|
|
68
|
+
): Promise<string> {
|
|
61
69
|
if (input.confirm !== true) {
|
|
62
70
|
throw new Error('Refusing to delete post without confirm=true after explicit user confirmation.');
|
|
63
71
|
}
|
|
64
|
-
const result = await client.deletePost(input.post_id);
|
|
72
|
+
const result = await client.deletePost(input.post_id, { confirmPublished: input.confirm_published === true });
|
|
73
|
+
const platformDelete = result.platform_delete ? ` Platform delete: ${result.platform_delete}.` : '';
|
|
65
74
|
return [
|
|
66
|
-
`Post #${result.id} deleted successfully
|
|
75
|
+
`Post #${result.id} deleted successfully.${platformDelete}`,
|
|
67
76
|
'It has been removed from the posterly queue.',
|
|
68
77
|
`View queue in posterly: ${dashboardUrlForPosts()}`,
|
|
69
78
|
'',
|
|
@@ -29,7 +29,7 @@ type ListAnalyticsInput = {
|
|
|
29
29
|
export const listAnalyticsTool = {
|
|
30
30
|
name: 'list_analytics',
|
|
31
31
|
description:
|
|
32
|
-
'Analytics for connected social accounts (Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, YouTube, and Threads). view=accounts (account_id): daily snapshots and a period summary with platform-native display_metrics labels. view=posts (account_id): per-post likes, comments/replies, reach, impressions/views, saves, shares, reposts, quotes, plays, clicks, and watch time, most recent first, with limit/offset. view=insights: per-post feedback insights with a performance tier (great/good/mixed/poor), diagnosis, next action, and baseline, filtered by account_id, post_id, or checkpoint (1h, 6h, 24h, 72h, 7d); read-only, Pro plan or higher. presentation (accounts and posts): compact for Telegram/mobile, table for Markdown clients, json for custom chart/card renderers.',
|
|
32
|
+
'Analytics for connected social accounts (Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, YouTube, and Threads). view=accounts (account_id): daily snapshots and a period summary with platform-native display_metrics labels. Google Business Profile account analytics includes Calls, Website clicks, and Directions along with Profile Views, Search Views, Maps Views, Customer Actions, and Posts. view=posts (account_id): per-post likes, comments/replies, reach, impressions/views, saves, shares, reposts, quotes, plays, clicks, and watch time, most recent first, with limit/offset. view=insights: per-post feedback insights with a performance tier (great/good/mixed/poor), diagnosis, next action, and baseline, filtered by account_id, post_id, or checkpoint (1h, 6h, 24h, 72h, 7d); read-only, Pro plan or higher. presentation (accounts and posts): compact for Telegram/mobile, table for Markdown clients, json for custom chart/card renderers.',
|
|
33
33
|
inputSchema: z.object({
|
|
34
34
|
view: z.enum(['accounts', 'posts', 'insights']).describe('accounts, posts, or insights.'),
|
|
35
35
|
account_id: z
|
|
@@ -194,6 +194,9 @@ function accountMetricRows(summary: AccountAnalyticsSummary, platform: string):
|
|
|
194
194
|
['Search Views', formatNumber(metrics.search_views ?? summary.total_reach)],
|
|
195
195
|
['Maps Views', formatNumber(metrics.maps_views ?? summary.total_profile_views)],
|
|
196
196
|
['Customer Actions', formatNumber(metrics.customer_actions ?? summary.total_accounts_engaged)],
|
|
197
|
+
['Calls', formatNumber(metrics.call_clicks ?? summary.total_call_clicks)],
|
|
198
|
+
['Directions', formatNumber(metrics.direction_requests ?? summary.total_direction_requests)],
|
|
199
|
+
['Website clicks', formatNumber(metrics.website_clicks ?? summary.total_website_clicks)],
|
|
197
200
|
['Posts', formatNumber(metrics.posts ?? summary.current_media_count)],
|
|
198
201
|
];
|
|
199
202
|
}
|
package/src/tools/list-posts.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { SUPPORTED_PLATFORM_INPUTS, SUPPORTED_PLATFORM_IDS } from '../generated/
|
|
|
6
6
|
export const listPostsTool = {
|
|
7
7
|
name: 'list_posts',
|
|
8
8
|
description:
|
|
9
|
-
'List upcoming or recent posts. Filter by status (scheduled, published, failed, draft), platform, account_id, workspace_id, approval_status, or brand_id. Returns posts in your workspaces, teammates\' too, per your role there (brand-locked seats only see their assigned brands). If workspace_id is omitted, this searches every workspace you belong to.',
|
|
9
|
+
'List upcoming or recent posts. Filter by status (scheduled, published, failed, draft), platform, account_id, workspace_id, approval_status, approval_waiting_on, or brand_id. approval_waiting_on=client is the same queue as approval_status=pending_client. Each post includes approval.pending_client_approval. Returns posts in your workspaces, teammates\' too, per your role there (brand-locked seats only see their assigned brands). If workspace_id is omitted, this searches every workspace you belong to.',
|
|
10
10
|
inputSchema: z.object({
|
|
11
11
|
status: z
|
|
12
12
|
.string()
|
|
@@ -34,6 +34,10 @@ export const listPostsTool = {
|
|
|
34
34
|
.enum(['none', 'pending_client', 'approved', 'changes_requested', 'rejected'])
|
|
35
35
|
.optional()
|
|
36
36
|
.describe('Filter by client review approval status. pending_client means the post is waiting on the client.'),
|
|
37
|
+
approval_waiting_on: z
|
|
38
|
+
.enum(['client', 'team'])
|
|
39
|
+
.optional()
|
|
40
|
+
.describe('client: approval_status pending_client. team: changes_requested or rejected. Conflicts with a non-overlapping approval_status.'),
|
|
37
41
|
brand_id: z
|
|
38
42
|
.string()
|
|
39
43
|
.optional()
|
|
@@ -49,6 +53,7 @@ export const listPostsTool = {
|
|
|
49
53
|
limit?: number;
|
|
50
54
|
workspace_id?: string;
|
|
51
55
|
approval_status?: string;
|
|
56
|
+
approval_waiting_on?: 'client' | 'team';
|
|
52
57
|
brand_id?: string;
|
|
53
58
|
}
|
|
54
59
|
) {
|
package/src/tools/update-post.ts
CHANGED
|
@@ -32,6 +32,31 @@ const instagramSettingsSchema = z.object({
|
|
|
32
32
|
reel_thumb_offset: z.number().nonnegative().optional(),
|
|
33
33
|
is_ai_generated: z.boolean().optional().describe('Label the parent Instagram media container as AI-generated. For carousels, set on the parent only.'),
|
|
34
34
|
audio_id: z.string().optional().describe('Licensed Instagram audio ID for Reels. Meta-linked Facebook Login accounts only.'),
|
|
35
|
+
caption_addon: z.union([
|
|
36
|
+
z.object({
|
|
37
|
+
type: z.enum(['none', 'poll', 'comment_prompt']).optional(),
|
|
38
|
+
kind: z.enum(['none', 'poll', 'comment_prompt']).optional(),
|
|
39
|
+
options: z.array(z.string()).max(4).optional(),
|
|
40
|
+
extended_duration: z.boolean().optional(),
|
|
41
|
+
poll_attachment: z.object({
|
|
42
|
+
option_a: z.string().optional(),
|
|
43
|
+
option_b: z.string().optional(),
|
|
44
|
+
option_c: z.string().optional(),
|
|
45
|
+
option_d: z.string().optional(),
|
|
46
|
+
options: z.array(z.string()).max(4).optional(),
|
|
47
|
+
}).optional(),
|
|
48
|
+
}),
|
|
49
|
+
z.null(),
|
|
50
|
+
]).optional().describe('Optional Instagram caption add-on. type is none, poll, or comment_prompt. A poll needs 2 to 4 options of 1 to 25 characters (an emoji counts as one) and the caption is the question. A comment prompt uses the caption as the prompt. Facebook Login only. Not available on Stories. At most one add-on per post. Pass null or type none to clear it.'),
|
|
51
|
+
poll_attachment: z.object({
|
|
52
|
+
option_a: z.string().optional(),
|
|
53
|
+
option_b: z.string().optional(),
|
|
54
|
+
option_c: z.string().optional(),
|
|
55
|
+
option_d: z.string().optional(),
|
|
56
|
+
options: z.array(z.string()).max(4).optional(),
|
|
57
|
+
}).optional().describe('Instagram poll choices. option_a and option_b are required, option_c and option_d are optional, and option_d requires option_c. Mutually exclusive with a comment prompt. The caption is the question. Serialised JSON must be 200 characters or fewer.'),
|
|
58
|
+
is_comment_prompt_used: z.boolean().optional().describe('Set true to attach an Instagram comment prompt. The caption is the prompt. Mutually exclusive with a poll. Facebook Login only. Not available on Stories.'),
|
|
59
|
+
is_poll_duration_extended: z.boolean().optional().describe('Set true only together with a poll, to request the longer Instagram poll duration.'),
|
|
35
60
|
});
|
|
36
61
|
|
|
37
62
|
const platformSettingsSchema = z.object({
|
|
@@ -108,6 +133,17 @@ const platformSettingsSchema = z.object({
|
|
|
108
133
|
paid_partnership: z.boolean().optional(),
|
|
109
134
|
is_ai_generated: z.boolean().optional(),
|
|
110
135
|
audio_id: z.string().optional(),
|
|
136
|
+
caption_addon: z.union([
|
|
137
|
+
z.object({
|
|
138
|
+
type: z.enum(['none', 'poll', 'comment_prompt']).optional(),
|
|
139
|
+
options: z.array(z.string()).max(4).optional(),
|
|
140
|
+
extended_duration: z.boolean().optional(),
|
|
141
|
+
}),
|
|
142
|
+
z.null(),
|
|
143
|
+
]).optional(),
|
|
144
|
+
poll_attachment: z.record(z.string(), z.unknown()).optional(),
|
|
145
|
+
is_comment_prompt_used: z.boolean().optional(),
|
|
146
|
+
is_poll_duration_extended: z.boolean().optional(),
|
|
111
147
|
ai_generated: z.boolean().optional(),
|
|
112
148
|
}).passthrough();
|
|
113
149
|
|
|
@@ -127,12 +163,15 @@ type UpdatePostInput = EditInput & {
|
|
|
127
163
|
status?: 'scheduled' | 'paused' | 'draft';
|
|
128
164
|
release_id?: string;
|
|
129
165
|
group_id?: string;
|
|
166
|
+
approval_action?: 'request' | 'approve' | 'request_changes' | 'reject';
|
|
167
|
+
approval_axis?: 'asset' | 'caption';
|
|
168
|
+
approval_note?: string;
|
|
130
169
|
};
|
|
131
170
|
|
|
132
171
|
export const updatePostTool = {
|
|
133
172
|
name: 'update_post',
|
|
134
173
|
description:
|
|
135
|
-
'Change a scheduled or draft post. DESTRUCTIVE WRITE. Edit (default): caption, media, schedule, post_type, or settings; overwrites the existing post; cannot edit published or currently publishing posts; if a client has already approved the post, a caption edit reopens caption approval, a media or thumbnail edit reopens asset approval, and a reschedule, account change or settings change reopens both. status (scheduled, paused, or draft, with scheduled_at when scheduling a draft): pause, resume, or move back to draft; send it on its own. release_id (with optional group_id): set or repair external release/group IDs; send it on its own.\n\n' +
|
|
174
|
+
'Change a scheduled or draft post. DESTRUCTIVE WRITE. Edit (default): caption, media, schedule, post_type, or settings; overwrites the existing post; cannot edit published or currently publishing posts; if a client has already approved the post, a caption edit reopens caption approval, a media or thumbnail edit reopens asset approval, and a reschedule, account change or settings change reopens both. status (scheduled, paused, or draft, with scheduled_at when scheduling a draft): pause, resume, or move back to draft; send it on its own. release_id (with optional group_id): set or repair external release/group IDs; send it on its own. approval_action (request, approve, request_changes, or reject): change client approval. Only a workspace owner or admin may do this. The actor is the API key user, never a brand guest. request emails the brand reviewers. approve, request_changes, and reject are team-side decisions. request_changes needs approval_note. Optional approval_axis is asset or caption; omit it for both. Send approval_action on its own.\n\n' +
|
|
136
175
|
'REQUIRED BEFORE CALLING: Show the user a side-by-side of the CURRENT post (caption, scheduled time, media, status) and the PROPOSED change, and get explicit confirmation ("yes update it", "apply those changes") before calling. Do not auto-edit posts based on a general instruction - each change needs its own confirmation.',
|
|
137
176
|
inputSchema: z.object({
|
|
138
177
|
post_id: z.number().describe('The post ID to update'),
|
|
@@ -170,6 +209,18 @@ export const updatePostTool = {
|
|
|
170
209
|
.optional()
|
|
171
210
|
.describe('Set an external release ID instead of editing the post. Send with post_id and optional group_id only.'),
|
|
172
211
|
group_id: z.string().min(1).max(160).optional().describe('With release_id only: an external group/campaign ID.'),
|
|
212
|
+
approval_action: z
|
|
213
|
+
.enum(['request', 'approve', 'request_changes', 'reject'])
|
|
214
|
+
.optional()
|
|
215
|
+
.describe('Change client approval instead of editing the post. Owner or admin only. The actor is the API key user, never a brand guest. Send on its own.'),
|
|
216
|
+
approval_axis: z
|
|
217
|
+
.enum(['asset', 'caption'])
|
|
218
|
+
.optional()
|
|
219
|
+
.describe('With approval_action approve, request_changes, or reject: which axis to decide. Omit for both. Not allowed with request.'),
|
|
220
|
+
approval_note: z
|
|
221
|
+
.string()
|
|
222
|
+
.optional()
|
|
223
|
+
.describe('Note stored with the approval change. Required for request_changes.'),
|
|
173
224
|
confirm: z.literal(true).describe('Must be true after explicit user confirmation of the proposed change.'),
|
|
174
225
|
}),
|
|
175
226
|
|
|
@@ -193,6 +244,15 @@ export const updatePostTool = {
|
|
|
193
244
|
const { mode, args } = validateArgs(ctx, 'update_post', input);
|
|
194
245
|
if (mode === 'status') return updateStatus(client, args as { post_id: number; status: 'scheduled' | 'paused' | 'draft'; scheduled_at?: string; confirm: boolean });
|
|
195
246
|
if (mode === 'release_id') return updateReleaseId(client, args as { post_id: number; release_id: string; group_id?: string; confirm: true });
|
|
247
|
+
if (mode === 'approval') {
|
|
248
|
+
return updateApproval(client, args as {
|
|
249
|
+
post_id: number;
|
|
250
|
+
approval_action: 'request' | 'approve' | 'request_changes' | 'reject';
|
|
251
|
+
approval_axis?: 'asset' | 'caption';
|
|
252
|
+
approval_note?: string;
|
|
253
|
+
confirm: true;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
196
256
|
return editPost(client, args as EditInput);
|
|
197
257
|
},
|
|
198
258
|
};
|
|
@@ -241,6 +301,27 @@ async function updateStatus(
|
|
|
241
301
|
return `Post #${input.post_id} status changed from ${transition.old_status || 'unknown'} to ${transition.new_status || input.status}.`;
|
|
242
302
|
}
|
|
243
303
|
|
|
304
|
+
async function updateApproval(
|
|
305
|
+
client: PosterlyClient,
|
|
306
|
+
input: {
|
|
307
|
+
post_id: number;
|
|
308
|
+
approval_action: 'request' | 'approve' | 'request_changes' | 'reject';
|
|
309
|
+
approval_axis?: 'asset' | 'caption';
|
|
310
|
+
approval_note?: string;
|
|
311
|
+
confirm: true;
|
|
312
|
+
},
|
|
313
|
+
): Promise<string> {
|
|
314
|
+
if (input.confirm !== true) {
|
|
315
|
+
throw new Error('Refusing to change client approval without confirm=true after explicit user confirmation. Only a workspace owner or admin can do this, and the actor is the API key user, never a brand guest.');
|
|
316
|
+
}
|
|
317
|
+
const result = await client.setPostApproval(input.post_id, {
|
|
318
|
+
action: input.approval_action,
|
|
319
|
+
axis: input.approval_axis,
|
|
320
|
+
note: input.approval_note,
|
|
321
|
+
});
|
|
322
|
+
return JSON.stringify(result, null, 2);
|
|
323
|
+
}
|
|
324
|
+
|
|
244
325
|
async function updateReleaseId(
|
|
245
326
|
client: PosterlyClient,
|
|
246
327
|
input: { post_id: number; release_id: string; group_id?: string; confirm: true },
|
package/tool-registry.json
CHANGED
|
@@ -736,6 +736,43 @@
|
|
|
736
736
|
"type": "string"
|
|
737
737
|
}
|
|
738
738
|
}
|
|
739
|
+
},
|
|
740
|
+
"approval": {
|
|
741
|
+
"label": "Request, approve, request changes, or reject client review",
|
|
742
|
+
"destructive": true,
|
|
743
|
+
"idempotent": true,
|
|
744
|
+
"openWorld": false,
|
|
745
|
+
"confirm": "confirm",
|
|
746
|
+
"when": [
|
|
747
|
+
"approval_action"
|
|
748
|
+
],
|
|
749
|
+
"params": {
|
|
750
|
+
"post_id": {
|
|
751
|
+
"type": "integer",
|
|
752
|
+
"min": 1,
|
|
753
|
+
"required": true
|
|
754
|
+
},
|
|
755
|
+
"approval_action": {
|
|
756
|
+
"type": "string",
|
|
757
|
+
"enum": [
|
|
758
|
+
"request",
|
|
759
|
+
"approve",
|
|
760
|
+
"request_changes",
|
|
761
|
+
"reject"
|
|
762
|
+
],
|
|
763
|
+
"required": true
|
|
764
|
+
},
|
|
765
|
+
"approval_axis": {
|
|
766
|
+
"type": "string",
|
|
767
|
+
"enum": [
|
|
768
|
+
"asset",
|
|
769
|
+
"caption"
|
|
770
|
+
]
|
|
771
|
+
},
|
|
772
|
+
"approval_note": {
|
|
773
|
+
"type": "string"
|
|
774
|
+
}
|
|
775
|
+
}
|
|
739
776
|
}
|
|
740
777
|
}
|
|
741
778
|
},
|
|
@@ -760,6 +797,9 @@
|
|
|
760
797
|
"type": "integer",
|
|
761
798
|
"min": 1,
|
|
762
799
|
"required": true
|
|
800
|
+
},
|
|
801
|
+
"confirm_published": {
|
|
802
|
+
"type": "boolean"
|
|
763
803
|
}
|
|
764
804
|
}
|
|
765
805
|
},
|