posterly-mcp-server 0.46.0 → 0.47.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 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.46.0` exposes 62 tools covering 94 actions. The hosted HTTP endpoint exposes the same catalog minus the three signup tools: 59 tools covering 91 actions.
126
+ `posterly-mcp-server@0.47.0` exposes 62 tools covering 95 actions. The hosted HTTP endpoint exposes the same catalog minus the three signup tools: 59 tools covering 92 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
 
@@ -211,13 +211,14 @@ Accounts, posting, media, schedule, analytics, and support. What most assistants
211
211
  | `edit` (default) | Edit caption, media, schedule, or settings |
212
212
  | `status` (pass `status`) | Pause, resume, schedule, or move to draft |
213
213
  | `release_id` (pass `release_id`) | Set a release or group ID |
214
+ | `approval` (pass `approval_action`) | Request, approve, request changes, or reject client review (owner or admin only) |
214
215
 
215
216
  **`delete_post`**: Delete posts
216
217
 
217
218
  | Mode (picked by params) | What it does |
218
219
  | --- | --- |
219
- | `post` (default) | Delete one post |
220
- | `group` (pass `group_id`) | Delete a post group (preview then confirm) |
220
+ | `post` (default) | Delete one post. Published Instagram or Facebook posts also need `confirm_published` |
221
+ | `group` (pass `group_id`) | Delete a post group (preview then confirm). Never deletes published posts |
221
222
 
222
223
  **`upload_media`**: Upload media
223
224
 
@@ -145,6 +145,7 @@ export interface ApiPostApproval {
145
145
  asset_status: ApiPostApprovalStatus;
146
146
  caption_status: ApiPostApprovalStatus;
147
147
  waiting_on: 'client' | 'team' | null;
148
+ pending_client_approval: boolean;
148
149
  requested_at: string | null;
149
150
  completed_at: string | null;
150
151
  }
@@ -1212,6 +1213,7 @@ export declare class PosterlyClient {
1212
1213
  offset?: number;
1213
1214
  workspace_id?: string;
1214
1215
  approval_status?: string;
1216
+ approval_waiting_on?: string;
1215
1217
  brand_id?: string;
1216
1218
  }): Promise<{
1217
1219
  posts: Post[];
@@ -1244,10 +1246,18 @@ export declare class PosterlyClient {
1244
1246
  }): Promise<{
1245
1247
  post: Post;
1246
1248
  }>;
1247
- deletePost(id: number): Promise<{
1249
+ deletePost(id: number, options?: {
1250
+ confirmPublished?: boolean;
1251
+ }): Promise<{
1248
1252
  deleted: boolean;
1249
1253
  id: number;
1254
+ platform_delete?: string;
1250
1255
  }>;
1256
+ setPostApproval(id: number, data: {
1257
+ action: 'request' | 'approve' | 'request_changes' | 'reject';
1258
+ axis?: 'asset' | 'caption';
1259
+ note?: string;
1260
+ }): Promise<Record<string, unknown>>;
1251
1261
  updatePostStatus(id: number, data: {
1252
1262
  status: 'scheduled' | 'paused' | 'draft';
1253
1263
  scheduled_at?: string;
@@ -340,6 +340,8 @@ export class PosterlyClient {
340
340
  searchParams.set('workspace_id', params.workspace_id);
341
341
  if (params?.approval_status)
342
342
  searchParams.set('approval_status', params.approval_status);
343
+ if (params?.approval_waiting_on)
344
+ searchParams.set('approval_waiting_on', params.approval_waiting_on);
343
345
  if (params?.brand_id)
344
346
  searchParams.set('brand_id', params.brand_id);
345
347
  const qs = searchParams.toString();
@@ -375,8 +377,16 @@ export class PosterlyClient {
375
377
  async updatePost(id, data) {
376
378
  return this.request('PATCH', `/posts/${id}`, data);
377
379
  }
378
- async deletePost(id) {
379
- return this.request('DELETE', `/posts/${id}`);
380
+ async deletePost(id, options) {
381
+ const qs = options?.confirmPublished ? '?confirm_published=true' : '';
382
+ return this.request('DELETE', `/posts/${id}${qs}`);
383
+ }
384
+ async setPostApproval(id, data) {
385
+ return this.request('POST', `/posts/${id}/approval`, {
386
+ action: data.action,
387
+ ...(data.axis ? { axis: data.axis } : {}),
388
+ ...(data.note ? { note: data.note } : {}),
389
+ });
380
390
  }
381
391
  async updatePostStatus(id, data) {
382
392
  return this.request('PUT', `/posts/${id}/status`, data);
@@ -1 +1 @@
1
- export declare const POSTERLY_MCP_VERSION = "0.46.0";
1
+ export declare const POSTERLY_MCP_VERSION = "0.47.0";
@@ -5,4 +5,4 @@
5
5
  // tool set (minus the intentional pre-auth signup tools that only this stdio
6
6
  // package exposes). `npm run check:mcp-parity` enforces both the version match
7
7
  // and the tool-list match, and runs in the pre-commit hook.
8
- export const POSTERLY_MCP_VERSION = '0.46.0';
8
+ export const POSTERLY_MCP_VERSION = '0.47.0';
@@ -972,11 +972,11 @@ export declare const createPostPayloadInputSchema: z.ZodObject<{
972
972
  username?: string | undefined;
973
973
  workspace_id?: string | undefined;
974
974
  account_id?: string | undefined;
975
+ caption?: string | undefined;
975
976
  scheduled_at?: string | undefined;
976
977
  media_url?: string | undefined;
977
978
  media_urls?: string[] | undefined;
978
979
  post_type?: string | undefined;
979
- caption?: string | undefined;
980
980
  thread_posts?: string[] | undefined;
981
981
  instagram_settings?: {
982
982
  post_type?: "post" | "feed" | "story" | "reel" | "carousel" | undefined;
@@ -1135,11 +1135,11 @@ export declare const createPostPayloadInputSchema: z.ZodObject<{
1135
1135
  username?: string | undefined;
1136
1136
  workspace_id?: string | undefined;
1137
1137
  account_id?: string | undefined;
1138
+ caption?: string | undefined;
1138
1139
  scheduled_at?: string | undefined;
1139
1140
  media_url?: string | undefined;
1140
1141
  media_urls?: string[] | undefined;
1141
1142
  post_type?: string | undefined;
1142
- caption?: string | undefined;
1143
1143
  thread_posts?: string[] | undefined;
1144
1144
  instagram_settings?: {
1145
1145
  post_type?: "post" | "feed" | "story" | "reel" | "carousel" | undefined;
@@ -1791,11 +1791,11 @@ export declare const createPostInputSchema: z.ZodObject<{
1791
1791
  username?: string | undefined;
1792
1792
  workspace_id?: string | undefined;
1793
1793
  account_id?: string | undefined;
1794
+ caption?: string | undefined;
1794
1795
  scheduled_at?: string | undefined;
1795
1796
  media_url?: string | undefined;
1796
1797
  media_urls?: string[] | undefined;
1797
1798
  post_type?: string | undefined;
1798
- caption?: string | undefined;
1799
1799
  thread_posts?: string[] | undefined;
1800
1800
  instagram_settings?: {
1801
1801
  post_type?: "post" | "feed" | "story" | "reel" | "carousel" | undefined;
@@ -1955,11 +1955,11 @@ export declare const createPostInputSchema: z.ZodObject<{
1955
1955
  username?: string | undefined;
1956
1956
  workspace_id?: string | undefined;
1957
1957
  account_id?: string | undefined;
1958
+ caption?: string | undefined;
1958
1959
  scheduled_at?: string | undefined;
1959
1960
  media_url?: string | undefined;
1960
1961
  media_urls?: string[] | undefined;
1961
1962
  post_type?: string | undefined;
1962
- caption?: string | undefined;
1963
1963
  thread_posts?: string[] | undefined;
1964
1964
  instagram_settings?: {
1965
1965
  post_type?: "post" | "feed" | "story" | "reel" | "carousel" | undefined;
@@ -2640,11 +2640,11 @@ export declare const createPostTool: {
2640
2640
  username?: string | undefined;
2641
2641
  workspace_id?: string | undefined;
2642
2642
  account_id?: string | undefined;
2643
+ caption?: string | undefined;
2643
2644
  scheduled_at?: string | undefined;
2644
2645
  media_url?: string | undefined;
2645
2646
  media_urls?: string[] | undefined;
2646
2647
  post_type?: string | undefined;
2647
- caption?: string | undefined;
2648
2648
  thread_posts?: string[] | undefined;
2649
2649
  instagram_settings?: {
2650
2650
  post_type?: "post" | "feed" | "story" | "reel" | "carousel" | undefined;
@@ -2804,11 +2804,11 @@ export declare const createPostTool: {
2804
2804
  username?: string | undefined;
2805
2805
  workspace_id?: string | undefined;
2806
2806
  account_id?: string | undefined;
2807
+ caption?: string | undefined;
2807
2808
  scheduled_at?: string | undefined;
2808
2809
  media_url?: string | undefined;
2809
2810
  media_urls?: string[] | undefined;
2810
2811
  post_type?: string | undefined;
2811
- caption?: string | undefined;
2812
2812
  thread_posts?: string[] | undefined;
2813
2813
  instagram_settings?: {
2814
2814
  post_type?: "post" | "feed" | "story" | "reel" | "carousel" | undefined;
@@ -499,11 +499,11 @@ export declare const createPostsBatchTool: {
499
499
  username?: string | undefined;
500
500
  workspace_id?: string | undefined;
501
501
  account_id?: string | undefined;
502
+ caption?: string | undefined;
502
503
  scheduled_at?: string | undefined;
503
504
  media_url?: string | undefined;
504
505
  media_urls?: string[] | undefined;
505
506
  post_type?: string | undefined;
506
- caption?: string | undefined;
507
507
  thread_posts?: string[] | undefined;
508
508
  instagram_settings?: {
509
509
  post_type?: "post" | "feed" | "story" | "reel" | "carousel" | undefined;
@@ -662,11 +662,11 @@ export declare const createPostsBatchTool: {
662
662
  username?: string | undefined;
663
663
  workspace_id?: string | undefined;
664
664
  account_id?: string | undefined;
665
+ caption?: string | undefined;
665
666
  scheduled_at?: string | undefined;
666
667
  media_url?: string | undefined;
667
668
  media_urls?: string[] | undefined;
668
669
  post_type?: string | undefined;
669
- caption?: string | undefined;
670
670
  thread_posts?: string[] | undefined;
671
671
  instagram_settings?: {
672
672
  post_type?: "post" | "feed" | "story" | "reel" | "carousel" | undefined;
@@ -829,11 +829,11 @@ export declare const createPostsBatchTool: {
829
829
  username?: string | undefined;
830
830
  workspace_id?: string | undefined;
831
831
  account_id?: string | undefined;
832
+ caption?: string | undefined;
832
833
  scheduled_at?: string | undefined;
833
834
  media_url?: string | undefined;
834
835
  media_urls?: string[] | undefined;
835
836
  post_type?: string | undefined;
836
- caption?: string | undefined;
837
837
  thread_posts?: string[] | undefined;
838
838
  instagram_settings?: {
839
839
  post_type?: "post" | "feed" | "story" | "reel" | "carousel" | undefined;
@@ -995,11 +995,11 @@ export declare const createPostsBatchTool: {
995
995
  username?: string | undefined;
996
996
  workspace_id?: string | undefined;
997
997
  account_id?: string | undefined;
998
+ caption?: string | undefined;
998
999
  scheduled_at?: string | undefined;
999
1000
  media_url?: string | undefined;
1000
1001
  media_urls?: string[] | undefined;
1001
1002
  post_type?: string | undefined;
1002
- caption?: string | undefined;
1003
1003
  thread_posts?: string[] | undefined;
1004
1004
  instagram_settings?: {
1005
1005
  post_type?: "post" | "feed" | "story" | "reel" | "carousel" | undefined;
@@ -4,6 +4,7 @@ import { type ToolContext } from '../lib/tool-runtime.js';
4
4
  type DeletePostInput = {
5
5
  post_id?: number;
6
6
  group_id?: string;
7
+ confirm_published?: boolean;
7
8
  confirm?: boolean;
8
9
  preview_id?: string;
9
10
  };
@@ -13,6 +14,7 @@ export declare const deletePostTool: {
13
14
  inputSchema: z.ZodObject<{
14
15
  post_id: z.ZodOptional<z.ZodNumber>;
15
16
  group_id: z.ZodOptional<z.ZodString>;
17
+ confirm_published: z.ZodOptional<z.ZodBoolean>;
16
18
  confirm: z.ZodOptional<z.ZodBoolean>;
17
19
  preview_id: z.ZodOptional<z.ZodString>;
18
20
  }, "strip", z.ZodTypeAny, {
@@ -20,11 +22,13 @@ export declare const deletePostTool: {
20
22
  preview_id?: string | undefined;
21
23
  post_id?: number | undefined;
22
24
  group_id?: string | undefined;
25
+ confirm_published?: boolean | undefined;
23
26
  }, {
24
27
  confirm?: boolean | undefined;
25
28
  preview_id?: string | undefined;
26
29
  post_id?: number | undefined;
27
30
  group_id?: string | undefined;
31
+ confirm_published?: boolean | undefined;
28
32
  }>;
29
33
  /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
30
34
  legacySchemas: {
@@ -3,10 +3,14 @@ import { code, dashboardUrlForPosts } from '../lib/format.js';
3
3
  import { previewThenConfirm, validateArgs } from '../lib/tool-runtime.js';
4
4
  export const deletePostTool = {
5
5
  name: 'delete_post',
6
- description: 'Delete posts. DESTRUCTIVE and IRREVERSIBLE: deleted posts and captions cannot be recovered, and published or currently publishing posts are never deleted. post_id: delete one scheduled or draft 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"). 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; 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.',
6
+ description: '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.',
7
7
  inputSchema: z.object({
8
8
  post_id: z.number().optional().describe('Delete this one post.'),
9
- group_id: z.string().min(1).max(160).optional().describe('Delete every unpublished post in this group or release instead.'),
9
+ group_id: z.string().min(1).max(160).optional().describe('Delete every unpublished post in this group or release instead. Never deletes published posts.'),
10
+ confirm_published: z
11
+ .boolean()
12
+ .optional()
13
+ .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.'),
10
14
  confirm: z
11
15
  .boolean()
12
16
  .optional()
@@ -33,7 +37,7 @@ export const deletePostTool = {
33
37
  // GET /api/v1/posts has no group filter, so the preview says in plain
34
38
  // words which posts match instead of listing them.
35
39
  describe: async () => [
36
- `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)}. Published posts are never deleted. This cannot be undone.`,
40
+ `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.`,
37
41
  'To see the posts first, call list_posts and check each post\'s group or release ID.',
38
42
  ].join('\n\n'),
39
43
  run: async () => {
@@ -49,9 +53,10 @@ async function deleteOne(client, input) {
49
53
  if (input.confirm !== true) {
50
54
  throw new Error('Refusing to delete post without confirm=true after explicit user confirmation.');
51
55
  }
52
- const result = await client.deletePost(input.post_id);
56
+ const result = await client.deletePost(input.post_id, { confirmPublished: input.confirm_published === true });
57
+ const platformDelete = result.platform_delete ? ` Platform delete: ${result.platform_delete}.` : '';
53
58
  return [
54
- `Post #${result.id} deleted successfully.`,
59
+ `Post #${result.id} deleted successfully.${platformDelete}`,
55
60
  'It has been removed from the posterly queue.',
56
61
  `View queue in posterly: ${dashboardUrlForPosts()}`,
57
62
  '',
@@ -4,7 +4,7 @@ import { validateArgs } from '../lib/tool-runtime.js';
4
4
  const presentationSchema = z.enum(['compact', 'table', 'json']).optional();
5
5
  export const listAnalyticsTool = {
6
6
  name: 'list_analytics',
7
- description: '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.',
7
+ description: '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.',
8
8
  inputSchema: z.object({
9
9
  view: z.enum(['accounts', 'posts', 'insights']).describe('accounts, posts, or insights.'),
10
10
  account_id: z
@@ -150,6 +150,9 @@ function accountMetricRows(summary, platform) {
150
150
  ['Search Views', formatNumber(metrics.search_views ?? summary.total_reach)],
151
151
  ['Maps Views', formatNumber(metrics.maps_views ?? summary.total_profile_views)],
152
152
  ['Customer Actions', formatNumber(metrics.customer_actions ?? summary.total_accounts_engaged)],
153
+ ['Calls', formatNumber(metrics.call_clicks ?? summary.total_call_clicks)],
154
+ ['Directions', formatNumber(metrics.direction_requests ?? summary.total_direction_requests)],
155
+ ['Website clicks', formatNumber(metrics.website_clicks ?? summary.total_website_clicks)],
153
156
  ['Posts', formatNumber(metrics.posts ?? summary.current_media_count)],
154
157
  ];
155
158
  }
@@ -10,6 +10,7 @@ export declare const listPostsTool: {
10
10
  limit: z.ZodOptional<z.ZodNumber>;
11
11
  workspace_id: z.ZodOptional<z.ZodString>;
12
12
  approval_status: z.ZodOptional<z.ZodEnum<["none", "pending_client", "approved", "changes_requested", "rejected"]>>;
13
+ approval_waiting_on: z.ZodOptional<z.ZodEnum<["client", "team"]>>;
13
14
  brand_id: z.ZodOptional<z.ZodString>;
14
15
  }, "strip", z.ZodTypeAny, {
15
16
  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;
@@ -18,6 +19,7 @@ export declare const listPostsTool: {
18
19
  account_id?: string | undefined;
19
20
  limit?: number | undefined;
20
21
  approval_status?: "none" | "pending_client" | "approved" | "changes_requested" | "rejected" | undefined;
22
+ approval_waiting_on?: "client" | "team" | undefined;
21
23
  brand_id?: string | undefined;
22
24
  }, {
23
25
  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;
@@ -26,6 +28,7 @@ export declare const listPostsTool: {
26
28
  account_id?: string | undefined;
27
29
  limit?: number | undefined;
28
30
  approval_status?: "none" | "pending_client" | "approved" | "changes_requested" | "rejected" | undefined;
31
+ approval_waiting_on?: "client" | "team" | undefined;
29
32
  brand_id?: string | undefined;
30
33
  }>;
31
34
  execute(client: PosterlyClient, input: {
@@ -35,6 +38,7 @@ export declare const listPostsTool: {
35
38
  limit?: number;
36
39
  workspace_id?: string;
37
40
  approval_status?: string;
41
+ approval_waiting_on?: "client" | "team";
38
42
  brand_id?: string;
39
43
  }): Promise<string>;
40
44
  };
@@ -3,7 +3,7 @@ import { dashboardUrlForPostList, formatLocalDateTime, truncateText } from '../l
3
3
  import { SUPPORTED_PLATFORM_INPUTS, SUPPORTED_PLATFORM_IDS } from '../generated/platform-manifest.js';
4
4
  export const listPostsTool = {
5
5
  name: 'list_posts',
6
- description: '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.',
6
+ description: '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.',
7
7
  inputSchema: z.object({
8
8
  status: z
9
9
  .string()
@@ -31,6 +31,10 @@ export const listPostsTool = {
31
31
  .enum(['none', 'pending_client', 'approved', 'changes_requested', 'rejected'])
32
32
  .optional()
33
33
  .describe('Filter by client review approval status. pending_client means the post is waiting on the client.'),
34
+ approval_waiting_on: z
35
+ .enum(['client', 'team'])
36
+ .optional()
37
+ .describe('client: approval_status pending_client. team: changes_requested or rejected. Conflicts with a non-overlapping approval_status.'),
34
38
  brand_id: z
35
39
  .string()
36
40
  .optional()
@@ -497,6 +497,9 @@ type UpdatePostInput = EditInput & {
497
497
  status?: 'scheduled' | 'paused' | 'draft';
498
498
  release_id?: string;
499
499
  group_id?: string;
500
+ approval_action?: 'request' | 'approve' | 'request_changes' | 'reject';
501
+ approval_axis?: 'asset' | 'caption';
502
+ approval_note?: string;
500
503
  };
501
504
  export declare const updatePostTool: {
502
505
  name: string;
@@ -995,11 +998,15 @@ export declare const updatePostTool: {
995
998
  status: z.ZodOptional<z.ZodEnum<["scheduled", "paused", "draft"]>>;
996
999
  release_id: z.ZodOptional<z.ZodString>;
997
1000
  group_id: z.ZodOptional<z.ZodString>;
1001
+ approval_action: z.ZodOptional<z.ZodEnum<["request", "approve", "request_changes", "reject"]>>;
1002
+ approval_axis: z.ZodOptional<z.ZodEnum<["asset", "caption"]>>;
1003
+ approval_note: z.ZodOptional<z.ZodString>;
998
1004
  confirm: z.ZodLiteral<true>;
999
1005
  }, "strip", z.ZodTypeAny, {
1000
1006
  confirm: true;
1001
1007
  post_id: number;
1002
1008
  status?: "scheduled" | "paused" | "draft" | undefined;
1009
+ caption?: string | undefined;
1003
1010
  scheduled_at?: string | undefined;
1004
1011
  media_url?: string | undefined;
1005
1012
  media_urls?: string[] | undefined;
@@ -1007,7 +1014,6 @@ export declare const updatePostTool: {
1007
1014
  reel_cover_url?: string | undefined;
1008
1015
  reel_cover_method?: string | undefined;
1009
1016
  reel_thumb_offset?: number | undefined;
1010
- caption?: string | undefined;
1011
1017
  instagram_settings?: {
1012
1018
  post_type?: "post" | "feed" | "story" | "reel" | "carousel" | undefined;
1013
1019
  reel_cover_url?: string | undefined;
@@ -1163,10 +1169,14 @@ export declare const updatePostTool: {
1163
1169
  }, z.ZodTypeAny, "passthrough"> | undefined;
1164
1170
  release_id?: string | undefined;
1165
1171
  group_id?: string | undefined;
1172
+ approval_action?: "request" | "approve" | "request_changes" | "reject" | undefined;
1173
+ approval_axis?: "asset" | "caption" | undefined;
1174
+ approval_note?: string | undefined;
1166
1175
  }, {
1167
1176
  confirm: true;
1168
1177
  post_id: number;
1169
1178
  status?: "scheduled" | "paused" | "draft" | undefined;
1179
+ caption?: string | undefined;
1170
1180
  scheduled_at?: string | undefined;
1171
1181
  media_url?: string | undefined;
1172
1182
  media_urls?: string[] | undefined;
@@ -1174,7 +1184,6 @@ export declare const updatePostTool: {
1174
1184
  reel_cover_url?: string | undefined;
1175
1185
  reel_cover_method?: string | undefined;
1176
1186
  reel_thumb_offset?: number | undefined;
1177
- caption?: string | undefined;
1178
1187
  instagram_settings?: {
1179
1188
  post_type?: "post" | "feed" | "story" | "reel" | "carousel" | undefined;
1180
1189
  reel_cover_url?: string | undefined;
@@ -1330,6 +1339,9 @@ export declare const updatePostTool: {
1330
1339
  }, z.ZodTypeAny, "passthrough"> | undefined;
1331
1340
  release_id?: string | undefined;
1332
1341
  group_id?: string | undefined;
1342
+ approval_action?: "request" | "approve" | "request_changes" | "reject" | undefined;
1343
+ approval_axis?: "asset" | "caption" | undefined;
1344
+ approval_note?: string | undefined;
1333
1345
  }>;
1334
1346
  /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
1335
1347
  legacySchemas: {
@@ -108,7 +108,7 @@ const platformSettingsSchema = z.object({
108
108
  }).passthrough();
109
109
  export const updatePostTool = {
110
110
  name: 'update_post',
111
- description: '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' +
111
+ description: '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' +
112
112
  '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.',
113
113
  inputSchema: z.object({
114
114
  post_id: z.number().describe('The post ID to update'),
@@ -146,6 +146,18 @@ export const updatePostTool = {
146
146
  .optional()
147
147
  .describe('Set an external release ID instead of editing the post. Send with post_id and optional group_id only.'),
148
148
  group_id: z.string().min(1).max(160).optional().describe('With release_id only: an external group/campaign ID.'),
149
+ approval_action: z
150
+ .enum(['request', 'approve', 'request_changes', 'reject'])
151
+ .optional()
152
+ .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.'),
153
+ approval_axis: z
154
+ .enum(['asset', 'caption'])
155
+ .optional()
156
+ .describe('With approval_action approve, request_changes, or reject: which axis to decide. Omit for both. Not allowed with request.'),
157
+ approval_note: z
158
+ .string()
159
+ .optional()
160
+ .describe('Note stored with the approval change. Required for request_changes.'),
149
161
  confirm: z.literal(true).describe('Must be true after explicit user confirmation of the proposed change.'),
150
162
  }),
151
163
  /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
@@ -169,6 +181,9 @@ export const updatePostTool = {
169
181
  return updateStatus(client, args);
170
182
  if (mode === 'release_id')
171
183
  return updateReleaseId(client, args);
184
+ if (mode === 'approval') {
185
+ return updateApproval(client, args);
186
+ }
172
187
  return editPost(client, args);
173
188
  },
174
189
  };
@@ -209,6 +224,17 @@ async function updateStatus(client, input) {
209
224
  const transition = result.transition || {};
210
225
  return `Post #${input.post_id} status changed from ${transition.old_status || 'unknown'} to ${transition.new_status || input.status}.`;
211
226
  }
227
+ async function updateApproval(client, input) {
228
+ if (input.confirm !== true) {
229
+ 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.');
230
+ }
231
+ const result = await client.setPostApproval(input.post_id, {
232
+ action: input.approval_action,
233
+ axis: input.approval_axis,
234
+ note: input.approval_note,
235
+ });
236
+ return JSON.stringify(result, null, 2);
237
+ }
212
238
  async function updateReleaseId(client, input) {
213
239
  const result = await client.updatePostReleaseId(input.post_id, {
214
240
  release_id: input.release_id,
@@ -494,11 +494,11 @@ export declare const validatePostInputSchema: import("zod").ZodObject<{
494
494
  username?: string | undefined;
495
495
  workspace_id?: string | undefined;
496
496
  account_id?: string | undefined;
497
+ caption?: string | undefined;
497
498
  scheduled_at?: string | undefined;
498
499
  media_url?: string | undefined;
499
500
  media_urls?: string[] | undefined;
500
501
  post_type?: string | undefined;
501
- caption?: string | undefined;
502
502
  thread_posts?: string[] | undefined;
503
503
  instagram_settings?: {
504
504
  post_type?: "post" | "feed" | "story" | "reel" | "carousel" | undefined;
@@ -657,11 +657,11 @@ export declare const validatePostInputSchema: import("zod").ZodObject<{
657
657
  username?: string | undefined;
658
658
  workspace_id?: string | undefined;
659
659
  account_id?: string | undefined;
660
+ caption?: string | undefined;
660
661
  scheduled_at?: string | undefined;
661
662
  media_url?: string | undefined;
662
663
  media_urls?: string[] | undefined;
663
664
  post_type?: string | undefined;
664
- caption?: string | undefined;
665
665
  thread_posts?: string[] | undefined;
666
666
  instagram_settings?: {
667
667
  post_type?: "post" | "feed" | "story" | "reel" | "carousel" | undefined;
@@ -1314,11 +1314,11 @@ export declare const validatePostTool: {
1314
1314
  username?: string | undefined;
1315
1315
  workspace_id?: string | undefined;
1316
1316
  account_id?: string | undefined;
1317
+ caption?: string | undefined;
1317
1318
  scheduled_at?: string | undefined;
1318
1319
  media_url?: string | undefined;
1319
1320
  media_urls?: string[] | undefined;
1320
1321
  post_type?: string | undefined;
1321
- caption?: string | undefined;
1322
1322
  thread_posts?: string[] | undefined;
1323
1323
  instagram_settings?: {
1324
1324
  post_type?: "post" | "feed" | "story" | "reel" | "carousel" | undefined;
@@ -1477,11 +1477,11 @@ export declare const validatePostTool: {
1477
1477
  username?: string | undefined;
1478
1478
  workspace_id?: string | undefined;
1479
1479
  account_id?: string | undefined;
1480
+ caption?: string | undefined;
1480
1481
  scheduled_at?: string | undefined;
1481
1482
  media_url?: string | undefined;
1482
1483
  media_urls?: string[] | undefined;
1483
1484
  post_type?: string | undefined;
1484
- caption?: string | undefined;
1485
1485
  thread_posts?: string[] | undefined;
1486
1486
  instagram_settings?: {
1487
1487
  post_type?: "post" | "feed" | "story" | "reel" | "carousel" | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posterly-mcp-server",
3
- "version": "0.46.0",
3
+ "version": "0.47.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.46.0",
6
+ "version": "0.47.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.46.0",
17
+ "version": "0.47.0",
18
18
  "transport": {
19
19
  "type": "stdio"
20
20
  },
@@ -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
  }
@@ -1544,6 +1545,7 @@ export class PosterlyClient {
1544
1545
  offset?: number;
1545
1546
  workspace_id?: string;
1546
1547
  approval_status?: string;
1548
+ approval_waiting_on?: string;
1547
1549
  brand_id?: string;
1548
1550
  }): Promise<{ posts: Post[]; total: number }> {
1549
1551
  const searchParams = new URLSearchParams();
@@ -1554,6 +1556,7 @@ export class PosterlyClient {
1554
1556
  if (params?.offset) searchParams.set('offset', String(params.offset));
1555
1557
  if (params?.workspace_id) searchParams.set('workspace_id', params.workspace_id);
1556
1558
  if (params?.approval_status) searchParams.set('approval_status', params.approval_status);
1559
+ if (params?.approval_waiting_on) searchParams.set('approval_waiting_on', params.approval_waiting_on);
1557
1560
  if (params?.brand_id) searchParams.set('brand_id', params.brand_id);
1558
1561
 
1559
1562
  const qs = searchParams.toString();
@@ -1614,8 +1617,27 @@ export class PosterlyClient {
1614
1617
  return this.request('PATCH', `/posts/${id}`, data);
1615
1618
  }
1616
1619
 
1617
- async deletePost(id: number): Promise<{ deleted: boolean; id: number }> {
1618
- return this.request('DELETE', `/posts/${id}`);
1620
+ async deletePost(
1621
+ id: number,
1622
+ options?: { confirmPublished?: boolean },
1623
+ ): Promise<{ deleted: boolean; id: number; platform_delete?: string }> {
1624
+ const qs = options?.confirmPublished ? '?confirm_published=true' : '';
1625
+ return this.request('DELETE', `/posts/${id}${qs}`);
1626
+ }
1627
+
1628
+ async setPostApproval(
1629
+ id: number,
1630
+ data: {
1631
+ action: 'request' | 'approve' | 'request_changes' | 'reject';
1632
+ axis?: 'asset' | 'caption';
1633
+ note?: string;
1634
+ },
1635
+ ): Promise<Record<string, unknown>> {
1636
+ return this.request('POST', `/posts/${id}/approval`, {
1637
+ action: data.action,
1638
+ ...(data.axis ? { axis: data.axis } : {}),
1639
+ ...(data.note ? { note: data.note } : {}),
1640
+ });
1619
1641
  }
1620
1642
 
1621
1643
  async updatePostStatus(
@@ -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.46.0';
8
+ export const POSTERLY_MCP_VERSION = '0.47.0';
@@ -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, and published or currently publishing posts are never deleted. post_id: delete one scheduled or draft 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"). 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; 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
+ '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)}. Published posts are never deleted. This cannot be undone.`,
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(client: PosterlyClient, input: { post_id: number; confirm?: boolean }): Promise<string> {
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
  }
@@ -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
  ) {
@@ -127,12 +127,15 @@ type UpdatePostInput = EditInput & {
127
127
  status?: 'scheduled' | 'paused' | 'draft';
128
128
  release_id?: string;
129
129
  group_id?: string;
130
+ approval_action?: 'request' | 'approve' | 'request_changes' | 'reject';
131
+ approval_axis?: 'asset' | 'caption';
132
+ approval_note?: string;
130
133
  };
131
134
 
132
135
  export const updatePostTool = {
133
136
  name: 'update_post',
134
137
  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' +
138
+ '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
139
  '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
140
  inputSchema: z.object({
138
141
  post_id: z.number().describe('The post ID to update'),
@@ -170,6 +173,18 @@ export const updatePostTool = {
170
173
  .optional()
171
174
  .describe('Set an external release ID instead of editing the post. Send with post_id and optional group_id only.'),
172
175
  group_id: z.string().min(1).max(160).optional().describe('With release_id only: an external group/campaign ID.'),
176
+ approval_action: z
177
+ .enum(['request', 'approve', 'request_changes', 'reject'])
178
+ .optional()
179
+ .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.'),
180
+ approval_axis: z
181
+ .enum(['asset', 'caption'])
182
+ .optional()
183
+ .describe('With approval_action approve, request_changes, or reject: which axis to decide. Omit for both. Not allowed with request.'),
184
+ approval_note: z
185
+ .string()
186
+ .optional()
187
+ .describe('Note stored with the approval change. Required for request_changes.'),
173
188
  confirm: z.literal(true).describe('Must be true after explicit user confirmation of the proposed change.'),
174
189
  }),
175
190
 
@@ -193,6 +208,15 @@ export const updatePostTool = {
193
208
  const { mode, args } = validateArgs(ctx, 'update_post', input);
194
209
  if (mode === 'status') return updateStatus(client, args as { post_id: number; status: 'scheduled' | 'paused' | 'draft'; scheduled_at?: string; confirm: boolean });
195
210
  if (mode === 'release_id') return updateReleaseId(client, args as { post_id: number; release_id: string; group_id?: string; confirm: true });
211
+ if (mode === 'approval') {
212
+ return updateApproval(client, args as {
213
+ post_id: number;
214
+ approval_action: 'request' | 'approve' | 'request_changes' | 'reject';
215
+ approval_axis?: 'asset' | 'caption';
216
+ approval_note?: string;
217
+ confirm: true;
218
+ });
219
+ }
196
220
  return editPost(client, args as EditInput);
197
221
  },
198
222
  };
@@ -241,6 +265,27 @@ async function updateStatus(
241
265
  return `Post #${input.post_id} status changed from ${transition.old_status || 'unknown'} to ${transition.new_status || input.status}.`;
242
266
  }
243
267
 
268
+ async function updateApproval(
269
+ client: PosterlyClient,
270
+ input: {
271
+ post_id: number;
272
+ approval_action: 'request' | 'approve' | 'request_changes' | 'reject';
273
+ approval_axis?: 'asset' | 'caption';
274
+ approval_note?: string;
275
+ confirm: true;
276
+ },
277
+ ): Promise<string> {
278
+ if (input.confirm !== true) {
279
+ 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.');
280
+ }
281
+ const result = await client.setPostApproval(input.post_id, {
282
+ action: input.approval_action,
283
+ axis: input.approval_axis,
284
+ note: input.approval_note,
285
+ });
286
+ return JSON.stringify(result, null, 2);
287
+ }
288
+
244
289
  async function updateReleaseId(
245
290
  client: PosterlyClient,
246
291
  input: { post_id: number; release_id: string; group_id?: string; confirm: true },
@@ -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
  },