posterly-mcp-server 0.48.3 → 0.48.5

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.48.3` exposes 62 tools covering 95 actions. The hosted HTTP endpoint exposes the same catalog minus the three signup tools: 59 tools covering 92 actions.
126
+ `posterly-mcp-server@0.48.5` exposes 64 tools covering 99 actions. The hosted HTTP endpoint exposes the same catalog minus the three signup tools: 61 tools covering 96 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
 
@@ -321,13 +321,14 @@ Google Business Profile reviews, media, and audits.
321
321
 
322
322
  ### Admin (`admin`)
323
323
 
324
- API keys, OAuth clients, webhooks, and workspace members.
324
+ API keys, OAuth clients, webhooks, workspace members, and the publishing pause.
325
325
 
326
326
  | Tool | What it does |
327
327
  | --- | --- |
328
328
  | `create_api_key` | Create an API key |
329
329
  | `delete_api_key` | Revoke an API key |
330
330
  | `list_workspace_members` | Workspace members |
331
+ | `list_publishing_pauses` | Publishing pause: whether the workspace or any brand is paused, and what you may do |
331
332
  | `list_oauth_clients` | OAuth clients |
332
333
  | `list_webhooks` | Webhooks |
333
334
  | `create_webhook` | Create a webhook |
@@ -340,6 +341,14 @@ API keys, OAuth clients, webhooks, and workspace members.
340
341
  | `update` | Change role or brands, or turn a teammate's API and AI access on or off (preview then confirm) |
341
342
  | `remove` | Remove a member or invite (preview then confirm) |
342
343
 
344
+ **`manage_publishing_pause`**: Pause or resume publishing
345
+
346
+ | Action | What it does |
347
+ | --- | --- |
348
+ | `pause` | Pause publishing for the workspace or one brand: call without `confirm` for a preview of what it holds, then `confirm: true` after the user agrees (publisher or higher) |
349
+ | `resume` | Resume publishing: future posts are scheduled again, posts that missed their time stay paused and are listed (admin or higher) |
350
+ | `publish_missed` | Publish missed posts now, with `confirm: true` after the user agrees (admin or higher) |
351
+
343
352
  **`manage_oauth_client`**: Manage an OAuth client
344
353
 
345
354
  | Action | What it does |
package/dist/index.js CHANGED
@@ -21,6 +21,8 @@ import { createApiKeyTool } from './tools/create-api-key.js';
21
21
  import { deleteApiKeyTool } from './tools/delete-api-key.js';
22
22
  import { listWorkspaceMembersTool } from './tools/list-workspace-members.js';
23
23
  import { manageWorkspaceMemberTool } from './tools/manage-workspace-member.js';
24
+ import { listPublishingPausesTool } from './tools/list-publishing-pauses.js';
25
+ import { managePublishingPauseTool } from './tools/manage-publishing-pause.js';
24
26
  import { getCreditsTool } from './tools/get-credits.js';
25
27
  import { getSubscriptionTool } from './tools/get-subscription.js';
26
28
  import { cancelSubscriptionTool } from './tools/cancel-subscription.js';
@@ -89,6 +91,8 @@ const STDIO_TOOLS = [
89
91
  deleteApiKeyTool,
90
92
  listWorkspaceMembersTool,
91
93
  manageWorkspaceMemberTool,
94
+ listPublishingPausesTool,
95
+ managePublishingPauseTool,
92
96
  getCreditsTool,
93
97
  getSubscriptionTool,
94
98
  cancelSubscriptionTool,
@@ -425,6 +425,61 @@ export type WorkspaceMemberConfirmFields = {
425
425
  confirm?: boolean;
426
426
  preview_id?: string;
427
427
  };
428
+ export type PublishingPauseState = {
429
+ workspace: {
430
+ id: string;
431
+ name: string | null;
432
+ paused: boolean;
433
+ paused_at: string | null;
434
+ };
435
+ brands: Array<{
436
+ id: string;
437
+ name: string | null;
438
+ paused: boolean;
439
+ paused_at: string;
440
+ }>;
441
+ viewer: {
442
+ role: string;
443
+ can_pause_workspace: boolean;
444
+ can_pause_brand: boolean;
445
+ can_resume: boolean;
446
+ can_publish_missed: boolean;
447
+ };
448
+ };
449
+ /**
450
+ * POST /publishing-pause. A pause without confirm: true changes nothing and
451
+ * answers 400 confirmation_required with a preview; the client returns that
452
+ * body (requires_confirmation: true) instead of throwing, so the tool can show
453
+ * it.
454
+ */
455
+ export type PublishingPauseChangeResponse = {
456
+ ok?: boolean;
457
+ requires_confirmation?: boolean;
458
+ code?: string;
459
+ error?: string;
460
+ action?: 'pause' | 'resume';
461
+ scope?: 'workspace' | 'brand';
462
+ workspace_id?: string;
463
+ workspace_name?: string | null;
464
+ brand_id?: string | null;
465
+ brand_name?: string | null;
466
+ already_paused?: boolean;
467
+ scheduled_posts_to_hold?: number | null;
468
+ held_count?: number;
469
+ resumed_count?: number;
470
+ missed_count?: number;
471
+ missed_post_ids?: number[];
472
+ still_held_count?: number;
473
+ disconnected_count?: number;
474
+ failed_post_ids?: number[];
475
+ message?: string;
476
+ };
477
+ export type PublishMissedResponse = {
478
+ ok: boolean;
479
+ started: number;
480
+ skipped: number;
481
+ message?: string;
482
+ };
428
483
  export type CreateApiKeyPayload = {
429
484
  name?: string;
430
485
  scopes?: ApiKeyScope[];
@@ -559,12 +614,24 @@ export type SuggestedSlotBasis = 'yours' | 'platform' | 'default';
559
614
  export interface SuggestedSlot extends Slot {
560
615
  basis: SuggestedSlotBasis;
561
616
  reason: string;
617
+ /** 1-5; confidence-capped by basis (yours up to 5, platform up to 4, default up to 3). */
618
+ stars: number;
619
+ /** Tooltip text for `stars`, distinct from `reason`. */
620
+ star_reason: string;
621
+ }
622
+ export interface SuggestedTimesWeeklyRating {
623
+ stars: number;
624
+ basis: SuggestedSlotBasis;
625
+ reason: string;
626
+ estimated: boolean;
562
627
  }
563
628
  export interface SuggestedSlotsResponse {
564
629
  timezone: string;
565
630
  basis: SuggestedSlotBasis;
566
631
  basis_label: string;
567
632
  slots: SuggestedSlot[];
633
+ /** Only present when include=weekly_ratings was passed. */
634
+ weekly_ratings?: SuggestedTimesWeeklyRating[];
568
635
  }
569
636
  export interface Workspace {
570
637
  id: string;
@@ -1147,6 +1214,20 @@ export declare class PosterlyClient {
1147
1214
  member_id?: string;
1148
1215
  invite_id?: string;
1149
1216
  }, data: WorkspaceMemberConfirmFields): Promise<WorkspaceMemberChangeResponse>;
1217
+ getPublishingPause(params?: {
1218
+ workspace_id?: string;
1219
+ }): Promise<PublishingPauseState>;
1220
+ changePublishingPause(data: {
1221
+ action: 'pause' | 'resume';
1222
+ brand_id?: string;
1223
+ workspace_id?: string;
1224
+ confirm?: boolean;
1225
+ }): Promise<PublishingPauseChangeResponse>;
1226
+ publishMissedPausedPosts(data: {
1227
+ post_ids: number[];
1228
+ workspace_id?: string;
1229
+ confirm: true;
1230
+ }): Promise<PublishMissedResponse>;
1150
1231
  getSubscription(): Promise<SubscriptionSummaryResponse>;
1151
1232
  getCredits(): Promise<CreditsSummaryResponse>;
1152
1233
  cancelSubscription(data: CancelSubscriptionPayload): Promise<CancelSubscriptionResponse>;
@@ -116,7 +116,7 @@ export class PosterlyClient {
116
116
  throw new Error('posterly access is not installed yet. Use start_signup to begin paid setup, or set POSTERLY_API_KEY after signup.');
117
117
  }
118
118
  }
119
- async request(method, path, body) {
119
+ async request(method, path, body, options) {
120
120
  this.requireApiKey();
121
121
  const url = `${this.baseUrl}/api/v1${path}`;
122
122
  const headers = {
@@ -144,6 +144,11 @@ export class PosterlyClient {
144
144
  const res = await fetch(url, init);
145
145
  if (!res.ok) {
146
146
  const err = await res.json().catch(() => ({ error: res.statusText }));
147
+ // A write that needs confirm: true answers 400 with a preview; callers
148
+ // that show previews get the body back instead of an error.
149
+ if (options?.acceptConfirmationPreview && res.status === 400 && err?.requires_confirmation === true) {
150
+ return err;
151
+ }
147
152
  throw new Error(formatApiError(res, err));
148
153
  }
149
154
  return res.json();
@@ -204,6 +209,19 @@ export class PosterlyClient {
204
209
  const qs = params.toString();
205
210
  return this.request('DELETE', `${path}${qs ? `?${qs}` : ''}`);
206
211
  }
212
+ async getPublishingPause(params) {
213
+ const query = new URLSearchParams();
214
+ if (params?.workspace_id)
215
+ query.set('workspace_id', params.workspace_id);
216
+ const qs = query.toString();
217
+ return this.request('GET', `/publishing-pause${qs ? `?${qs}` : ''}`);
218
+ }
219
+ async changePublishingPause(data) {
220
+ return this.request('POST', '/publishing-pause', data, { acceptConfirmationPreview: true });
221
+ }
222
+ async publishMissedPausedPosts(data) {
223
+ return this.request('POST', '/publishing-pause/publish-missed', data);
224
+ }
207
225
  async getSubscription() {
208
226
  return this.request('GET', '/subscription');
209
227
  }
@@ -7,6 +7,13 @@ export declare function formatLocalDateTime(value?: string | null): string;
7
7
  export declare function dashboardUrlForPost(post: Pick<Post, 'id' | 'scheduled_at'>): string;
8
8
  export declare function dashboardUrlForPosts(): string;
9
9
  export declare function dashboardUrlForPostList(posts: Array<Pick<Post, 'scheduled_at'>>): string;
10
+ /**
11
+ * Renders a 1-5 star rating as filled/empty glyphs, e.g. 4 -> "★★★★☆".
12
+ * Appends " (est.)" for a default-basis rating, matching the dashboard's
13
+ * "Estimated" badge, so a thin or absent data signal reads the same way on
14
+ * both surfaces.
15
+ */
16
+ export declare function starGlyphs(stars: unknown, estimated?: boolean): string;
10
17
  export declare function truncateText(value: string | null | undefined, max?: number): string;
11
18
  export declare function formatAccountLabel(account: Account): string;
12
19
  export declare function formatConnectedAccounts(accounts?: Account[]): string;
@@ -25,6 +25,17 @@ export function dashboardUrlForPostList(posts) {
25
25
  }
26
26
  return DASHBOARD_TABLE_URL;
27
27
  }
28
+ /**
29
+ * Renders a 1-5 star rating as filled/empty glyphs, e.g. 4 -> "★★★★☆".
30
+ * Appends " (est.)" for a default-basis rating, matching the dashboard's
31
+ * "Estimated" badge, so a thin or absent data signal reads the same way on
32
+ * both surfaces.
33
+ */
34
+ export function starGlyphs(stars, estimated = false) {
35
+ const n = Math.max(0, Math.min(5, Math.round(Number(stars)) || 0));
36
+ const glyphs = '★'.repeat(n) + '☆'.repeat(5 - n);
37
+ return estimated ? `${glyphs} (est.)` : glyphs;
38
+ }
28
39
  export function truncateText(value, max = 80) {
29
40
  const text = value?.trim() || '';
30
41
  if (!text)
@@ -1 +1 @@
1
- export declare const POSTERLY_MCP_VERSION = "0.48.3";
1
+ export declare const POSTERLY_MCP_VERSION = "0.48.5";
@@ -5,4 +5,4 @@
5
5
  // tool set (minus the intentional pre-auth signup tools that only this stdio
6
6
  // package exposes). `npm run check:mcp-parity` enforces both the version match
7
7
  // and the tool-list match, and runs in the pre-commit hook.
8
- export const POSTERLY_MCP_VERSION = '0.48.3';
8
+ export const POSTERLY_MCP_VERSION = '0.48.5';
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { formatDate, mdEmpty, mdTable, mdTitle } from '../lib/format.js';
2
+ import { formatDate, mdEmpty, mdTable, mdTitle, starGlyphs } from '../lib/format.js';
3
3
  const SUGGESTED_BASIS_LABELS = {
4
4
  yours: 'Based on your posts',
5
5
  platform: 'Popular on this platform',
@@ -7,7 +7,7 @@ const SUGGESTED_BASIS_LABELS = {
7
7
  };
8
8
  export const findSlotTool = {
9
9
  name: 'find_available_slot',
10
- description: 'Find available time slots for posting. Respects a 1-hour gap between posts and preferred hours (8am-10pm in the given timezone). Returns up to 10 slots. IMPORTANT: pass a timezone explicitly - default is America/New_York and slots will be off if the user is elsewhere. Pass workspace_id to only avoid collisions with posts in that workspace. strategy "best_engagement" (needs account_ids) ranks the next 7 days by when these accounts get the most engagement, with a reason per slot.',
10
+ description: 'Find available time slots for posting. Respects a 1-hour gap between posts and preferred hours (8am-10pm in the given timezone). Returns up to 10 slots. IMPORTANT: pass a timezone explicitly - default is America/New_York and slots will be off if the user is elsewhere. Pass workspace_id to only avoid collisions with posts in that workspace. strategy "best_engagement" (needs account_ids) ranks the next 7 days by when these accounts get the most engagement, with a 1-5 star rating and a reason per slot.',
11
11
  inputSchema: z.object({
12
12
  account_ids: z
13
13
  .array(z.string())
@@ -54,9 +54,10 @@ export const findSlotTool = {
54
54
  `Basis: ${basis}${result.timezone ? ` (${result.timezone})` : ''}`,
55
55
  // local_time already carries the date in the requested timezone, so
56
56
  // no separate Date column (a machine-local date could disagree).
57
- mdTable(['Option', 'Local time', 'Why'], slots.map((slot, index) => [
57
+ mdTable(['Option', 'Local time', 'Rating', 'Why'], slots.map((slot, index) => [
58
58
  index + 1,
59
59
  slot.local_time,
60
+ starGlyphs(slot.stars, slot.basis === 'default'),
60
61
  slot.reason || 'n/a',
61
62
  ])),
62
63
  ].join('\n\n');
@@ -16,11 +16,11 @@ export declare const listBrandsTool: {
16
16
  }, "strip", z.ZodTypeAny, {
17
17
  workspace_id?: string | undefined;
18
18
  brand_id?: string | undefined;
19
- view?: "accounts" | "brands" | "brand" | "profile" | undefined;
19
+ view?: "brand" | "accounts" | "brands" | "profile" | undefined;
20
20
  }, {
21
21
  workspace_id?: string | undefined;
22
22
  brand_id?: string | undefined;
23
- view?: "accounts" | "brands" | "brand" | "profile" | undefined;
23
+ view?: "brand" | "accounts" | "brands" | "profile" | undefined;
24
24
  }>;
25
25
  /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
26
26
  legacySchemas: {
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient, PublishingPauseChangeResponse, PublishingPauseState, PublishMissedResponse } from '../lib/api-client.js';
3
+ export declare function formatPublishingPauseState(data: PublishingPauseState): string;
4
+ export declare function formatPublishingPauseChange(data: PublishingPauseChangeResponse): string;
5
+ export declare function formatPublishMissed(data: PublishMissedResponse): string;
6
+ export declare const listPublishingPausesTool: {
7
+ name: string;
8
+ description: string;
9
+ inputSchema: z.ZodObject<{
10
+ workspace_id: z.ZodOptional<z.ZodString>;
11
+ }, "strip", z.ZodTypeAny, {
12
+ workspace_id?: string | undefined;
13
+ }, {
14
+ workspace_id?: string | undefined;
15
+ }>;
16
+ execute(client: PosterlyClient, input: {
17
+ workspace_id?: string;
18
+ }): Promise<string>;
19
+ };
@@ -0,0 +1,81 @@
1
+ import { z } from 'zod';
2
+ // Plain-text output shared by list_publishing_pauses and
3
+ // manage_publishing_pause. The hosted endpoint (lib/mcp/http-tools.ts) renders
4
+ // the same text from the same API responses; scripts/test-publishing-pause-api.ts
5
+ // checks the two surfaces match.
6
+ function yesNo(value) {
7
+ return value ? 'yes' : 'no';
8
+ }
9
+ function idList(ids) {
10
+ return Array.isArray(ids) && ids.length > 0 ? ids.join(', ') : 'none';
11
+ }
12
+ function scopeText(data) {
13
+ if (data.scope === 'brand')
14
+ return `brand ${data.brand_name ? `${data.brand_name} ` : ''}(${data.brand_id})`;
15
+ return `the workspace ${data.workspace_name ? `${data.workspace_name} ` : ''}(${data.workspace_id})`;
16
+ }
17
+ export function formatPublishingPauseState(data) {
18
+ const workspace = data.workspace;
19
+ const brands = Array.isArray(data.brands) ? data.brands : [];
20
+ const viewer = data.viewer;
21
+ return [
22
+ `Publishing pause for ${workspace.name || 'this workspace'} (${workspace.id})`,
23
+ `Workspace: ${workspace.paused ? `paused since ${workspace.paused_at}` : 'publishing normally'}`,
24
+ brands.length > 0
25
+ ? ['Paused brands:', ...brands.map((brand) => `- ${brand.name || 'Unnamed brand'} (${brand.id}), paused since ${brand.paused_at}`)].join('\n')
26
+ : 'Paused brands: none',
27
+ `Your role: ${viewer.role}. Pause the workspace: ${yesNo(viewer.can_pause_workspace)}. Pause a brand: ${yesNo(viewer.can_pause_brand)}. Resume and publish missed posts: ${yesNo(viewer.can_resume)}.`,
28
+ 'While paused, nothing in that scope publishes (scheduled posts are held, Post now is refused). Pause and resume with manage_publishing_pause.',
29
+ ].join('\n');
30
+ }
31
+ export function formatPublishingPauseChange(data) {
32
+ if (data.requires_confirmation) {
33
+ return [
34
+ 'Preview only: nothing has changed yet.',
35
+ data.error || '',
36
+ `Scope: ${scopeText(data)}`,
37
+ `Scheduled posts to hold: ${typeof data.scheduled_posts_to_hold === 'number' ? data.scheduled_posts_to_hold : 'unknown'}`,
38
+ `Already paused: ${yesNo(data.already_paused)}`,
39
+ 'Next step: show this to the user. Only after they agree, call manage_publishing_pause again with action=pause, the same fields, and confirm=true.',
40
+ ].filter(Boolean).join('\n');
41
+ }
42
+ if (data.action === 'pause') {
43
+ const failed = data.failed_post_ids || [];
44
+ return [
45
+ `Publishing paused for ${scopeText(data)}.`,
46
+ `Held posts: ${data.held_count ?? 0}`,
47
+ failed.length > 0 ? `Posts that could not be marked as held (the publish guard still holds them): ${idList(failed)}` : '',
48
+ data.message || '',
49
+ ].filter(Boolean).join('\n');
50
+ }
51
+ const missed = data.missed_post_ids || [];
52
+ const failed = data.failed_post_ids || [];
53
+ return [
54
+ `Publishing resumed for ${scopeText(data)}.`,
55
+ `Scheduled again: ${data.resumed_count ?? 0}`,
56
+ `Missed their time while paused (still paused): ${missed.length}${missed.length > 0 ? ` [post IDs: ${idList(missed)}]` : ''}`,
57
+ `Still held by another pause: ${data.still_held_count ?? 0}`,
58
+ (data.disconnected_count ?? 0) > 0 ? `Still paused because the account is disconnected: ${data.disconnected_count}` : '',
59
+ failed.length > 0 ? `Could not be released (try resume again): ${idList(failed)}` : '',
60
+ missed.length > 0
61
+ ? 'Next step: missed posts never publish on their own. Ask the user whether to publish them now (action=publish_missed with these post_ids and confirm=true) or reschedule them.'
62
+ : '',
63
+ ].filter(Boolean).join('\n');
64
+ }
65
+ export function formatPublishMissed(data) {
66
+ return [
67
+ `Missed posts: ${data.started} publishing now, ${data.skipped} skipped.`,
68
+ data.message || '',
69
+ ].filter(Boolean).join('\n');
70
+ }
71
+ export const listPublishingPausesTool = {
72
+ name: 'list_publishing_pauses',
73
+ description: 'Is publishing paused? Shows whether the workspace is paused, which brands are paused (only brands you can see), and whether you may pause, resume, or publish missed posts. Read-only. Change it with manage_publishing_pause.',
74
+ inputSchema: z.object({
75
+ workspace_id: z.string().optional().describe('Workspace ID from whoami. Defaults to your default workspace.'),
76
+ }),
77
+ async execute(client, input) {
78
+ const state = await client.getPublishingPause(input.workspace_id ? { workspace_id: input.workspace_id } : undefined);
79
+ return formatPublishingPauseState(state);
80
+ },
81
+ };
@@ -0,0 +1,35 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { type ToolContext } from '../lib/tool-runtime.js';
4
+ type ManagePublishingPauseInput = {
5
+ action: 'pause' | 'resume' | 'publish_missed';
6
+ workspace_id?: string;
7
+ brand_id?: string;
8
+ post_ids?: number[];
9
+ confirm?: boolean;
10
+ };
11
+ export declare const managePublishingPauseTool: {
12
+ name: string;
13
+ description: string;
14
+ inputSchema: z.ZodObject<{
15
+ action: z.ZodEnum<["pause", "resume", "publish_missed"]>;
16
+ workspace_id: z.ZodOptional<z.ZodString>;
17
+ brand_id: z.ZodOptional<z.ZodString>;
18
+ post_ids: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
19
+ confirm: z.ZodOptional<z.ZodBoolean>;
20
+ }, "strip", z.ZodTypeAny, {
21
+ action: "pause" | "resume" | "publish_missed";
22
+ confirm?: boolean | undefined;
23
+ workspace_id?: string | undefined;
24
+ brand_id?: string | undefined;
25
+ post_ids?: number[] | undefined;
26
+ }, {
27
+ action: "pause" | "resume" | "publish_missed";
28
+ confirm?: boolean | undefined;
29
+ workspace_id?: string | undefined;
30
+ brand_id?: string | undefined;
31
+ post_ids?: number[] | undefined;
32
+ }>;
33
+ execute(client: PosterlyClient, input: ManagePublishingPauseInput, ctx: ToolContext): Promise<string>;
34
+ };
35
+ export {};
@@ -0,0 +1,47 @@
1
+ import { z } from 'zod';
2
+ import { validateArgs } from '../lib/tool-runtime.js';
3
+ import { formatPublishingPauseChange, formatPublishMissed } from './list-publishing-pauses.js';
4
+ /** Same wording as EMPTY_BRAND_ID_MESSAGE in lib/publish/publishing-pause-service.ts. */
5
+ const EMPTY_BRAND_ID_MESSAGE = 'brand_id is empty; omit it to target the whole workspace.';
6
+ export const managePublishingPauseTool = {
7
+ name: 'manage_publishing_pause',
8
+ description: 'Pause or resume publishing for a whole workspace or one brand (check it first with list_publishing_pauses). Only act when the user asks; never pause on your own initiative. action=pause (optional brand_id; leave it out for the whole workspace): holds every scheduled post in that scope and blocks Post now until an admin resumes. Call it first without confirm to get a preview (scope and how many scheduled posts it holds), show it to the user, and only after they agree call again with confirm=true. Publisher or higher; a seat limited to some brands can only pause those brands. action=resume (optional brand_id; admin or higher): future posts are scheduled again; posts whose time passed while paused stay paused and are listed as missed. Missed posts never publish on their own. action=publish_missed (post_ids from the resume, confirm=true after the user agrees; admin or higher): publishes those missed posts now.',
9
+ inputSchema: z.object({
10
+ action: z.enum(['pause', 'resume', 'publish_missed']).describe('pause, resume, or publish_missed.'),
11
+ workspace_id: z.string().optional().describe('Workspace ID from whoami. Defaults to your default workspace.'),
12
+ brand_id: z.string().optional().describe('pause and resume: a brand ID from list_brands. Leave it out (never empty) for the whole workspace.'),
13
+ post_ids: z.array(z.number().int().positive()).max(500).optional().describe('publish_missed: the missed post IDs a resume returned.'),
14
+ confirm: z.boolean().optional().describe('pause: leave out to get a preview; true only after the user agreed. publish_missed: true after the user agreed. resume: not needed.'),
15
+ }),
16
+ async execute(client, input, ctx) {
17
+ // A present but empty brand_id is refused, never read as the whole
18
+ // workspace (same rule and wording as the API and the hosted endpoint).
19
+ if ((input?.action === 'pause' || input?.action === 'resume') && input && Object.prototype.hasOwnProperty.call(input, 'brand_id')) {
20
+ const value = input.brand_id;
21
+ if (value === null || (typeof value === 'string' && value.trim() === ''))
22
+ throw new Error(EMPTY_BRAND_ID_MESSAGE);
23
+ }
24
+ const { mode, args } = validateArgs(ctx, 'manage_publishing_pause', input);
25
+ const call = args;
26
+ const workspace = call.workspace_id ? { workspace_id: call.workspace_id } : {};
27
+ if (mode === 'publish_missed') {
28
+ if (call.confirm !== true) {
29
+ throw new Error('Refusing to publish missed posts without confirm=true after explicit user confirmation.');
30
+ }
31
+ const result = await client.publishMissedPausedPosts({ ...workspace, post_ids: call.post_ids || [], confirm: true });
32
+ return formatPublishMissed(result);
33
+ }
34
+ const brand = call.brand_id ? { brand_id: call.brand_id } : {};
35
+ if (mode === 'pause') {
36
+ const result = await client.changePublishingPause({
37
+ action: 'pause',
38
+ ...workspace,
39
+ ...brand,
40
+ ...(call.confirm === true ? { confirm: true } : {}),
41
+ });
42
+ return formatPublishingPauseChange(result);
43
+ }
44
+ const result = await client.changePublishingPause({ action: 'resume', ...workspace, ...brand });
45
+ return formatPublishingPauseChange(result);
46
+ },
47
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posterly-mcp-server",
3
- "version": "0.48.3",
3
+ "version": "0.48.5",
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.48.3",
6
+ "version": "0.48.5",
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.48.3",
17
+ "version": "0.48.5",
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,
@@ -469,6 +469,54 @@ export type WorkspaceMemberConfirmFields = {
469
469
  preview_id?: string;
470
470
  };
471
471
 
472
+ export type PublishingPauseState = {
473
+ workspace: { id: string; name: string | null; paused: boolean; paused_at: string | null };
474
+ brands: Array<{ id: string; name: string | null; paused: boolean; paused_at: string }>;
475
+ viewer: {
476
+ role: string;
477
+ can_pause_workspace: boolean;
478
+ can_pause_brand: boolean;
479
+ can_resume: boolean;
480
+ can_publish_missed: boolean;
481
+ };
482
+ };
483
+
484
+ /**
485
+ * POST /publishing-pause. A pause without confirm: true changes nothing and
486
+ * answers 400 confirmation_required with a preview; the client returns that
487
+ * body (requires_confirmation: true) instead of throwing, so the tool can show
488
+ * it.
489
+ */
490
+ export type PublishingPauseChangeResponse = {
491
+ ok?: boolean;
492
+ requires_confirmation?: boolean;
493
+ code?: string;
494
+ error?: string;
495
+ action?: 'pause' | 'resume';
496
+ scope?: 'workspace' | 'brand';
497
+ workspace_id?: string;
498
+ workspace_name?: string | null;
499
+ brand_id?: string | null;
500
+ brand_name?: string | null;
501
+ already_paused?: boolean;
502
+ scheduled_posts_to_hold?: number | null;
503
+ held_count?: number;
504
+ resumed_count?: number;
505
+ missed_count?: number;
506
+ missed_post_ids?: number[];
507
+ still_held_count?: number;
508
+ disconnected_count?: number;
509
+ failed_post_ids?: number[];
510
+ message?: string;
511
+ };
512
+
513
+ export type PublishMissedResponse = {
514
+ ok: boolean;
515
+ started: number;
516
+ skipped: number;
517
+ message?: string;
518
+ };
519
+
472
520
  export type CreateApiKeyPayload = {
473
521
  name?: string;
474
522
  scopes?: ApiKeyScope[];
@@ -611,6 +659,17 @@ export type SuggestedSlotBasis = 'yours' | 'platform' | 'default';
611
659
  export interface SuggestedSlot extends Slot {
612
660
  basis: SuggestedSlotBasis;
613
661
  reason: string;
662
+ /** 1-5; confidence-capped by basis (yours up to 5, platform up to 4, default up to 3). */
663
+ stars: number;
664
+ /** Tooltip text for `stars`, distinct from `reason`. */
665
+ star_reason: string;
666
+ }
667
+
668
+ export interface SuggestedTimesWeeklyRating {
669
+ stars: number;
670
+ basis: SuggestedSlotBasis;
671
+ reason: string;
672
+ estimated: boolean;
614
673
  }
615
674
 
616
675
  export interface SuggestedSlotsResponse {
@@ -618,6 +677,8 @@ export interface SuggestedSlotsResponse {
618
677
  basis: SuggestedSlotBasis;
619
678
  basis_label: string;
620
679
  slots: SuggestedSlot[];
680
+ /** Only present when include=weekly_ratings was passed. */
681
+ weekly_ratings?: SuggestedTimesWeeklyRating[];
621
682
  }
622
683
 
623
684
  export interface Workspace {
@@ -1277,6 +1338,7 @@ export class PosterlyClient {
1277
1338
  method: string,
1278
1339
  path: string,
1279
1340
  body?: unknown,
1341
+ options?: { acceptConfirmationPreview?: boolean },
1280
1342
  ): Promise<T> {
1281
1343
  this.requireApiKey();
1282
1344
 
@@ -1309,6 +1371,11 @@ export class PosterlyClient {
1309
1371
 
1310
1372
  if (!res.ok) {
1311
1373
  const err = await res.json().catch(() => ({ error: res.statusText }));
1374
+ // A write that needs confirm: true answers 400 with a preview; callers
1375
+ // that show previews get the body back instead of an error.
1376
+ if (options?.acceptConfirmationPreview && res.status === 400 && err?.requires_confirmation === true) {
1377
+ return err as T;
1378
+ }
1312
1379
  throw new Error(formatApiError(res, err));
1313
1380
  }
1314
1381
 
@@ -1398,6 +1465,30 @@ export class PosterlyClient {
1398
1465
  return this.request('DELETE', `${path}${qs ? `?${qs}` : ''}`);
1399
1466
  }
1400
1467
 
1468
+ async getPublishingPause(params?: { workspace_id?: string }): Promise<PublishingPauseState> {
1469
+ const query = new URLSearchParams();
1470
+ if (params?.workspace_id) query.set('workspace_id', params.workspace_id);
1471
+ const qs = query.toString();
1472
+ return this.request('GET', `/publishing-pause${qs ? `?${qs}` : ''}`);
1473
+ }
1474
+
1475
+ async changePublishingPause(data: {
1476
+ action: 'pause' | 'resume';
1477
+ brand_id?: string;
1478
+ workspace_id?: string;
1479
+ confirm?: boolean;
1480
+ }): Promise<PublishingPauseChangeResponse> {
1481
+ return this.request('POST', '/publishing-pause', data, { acceptConfirmationPreview: true });
1482
+ }
1483
+
1484
+ async publishMissedPausedPosts(data: {
1485
+ post_ids: number[];
1486
+ workspace_id?: string;
1487
+ confirm: true;
1488
+ }): Promise<PublishMissedResponse> {
1489
+ return this.request('POST', '/publishing-pause/publish-missed', data);
1490
+ }
1491
+
1401
1492
  async getSubscription(): Promise<SubscriptionSummaryResponse> {
1402
1493
  return this.request('GET', '/subscription');
1403
1494
  }
package/src/lib/format.ts CHANGED
@@ -33,6 +33,18 @@ export function dashboardUrlForPostList(posts: Array<Pick<Post, 'scheduled_at'>>
33
33
  return DASHBOARD_TABLE_URL;
34
34
  }
35
35
 
36
+ /**
37
+ * Renders a 1-5 star rating as filled/empty glyphs, e.g. 4 -> "★★★★☆".
38
+ * Appends " (est.)" for a default-basis rating, matching the dashboard's
39
+ * "Estimated" badge, so a thin or absent data signal reads the same way on
40
+ * both surfaces.
41
+ */
42
+ export function starGlyphs(stars: unknown, estimated = false): string {
43
+ const n = Math.max(0, Math.min(5, Math.round(Number(stars)) || 0));
44
+ const glyphs = '★'.repeat(n) + '☆'.repeat(5 - n);
45
+ return estimated ? `${glyphs} (est.)` : glyphs;
46
+ }
47
+
36
48
  export function truncateText(value: string | null | undefined, max = 80): string {
37
49
  const text = value?.trim() || '';
38
50
  if (!text) return '(empty)';
@@ -5,4 +5,4 @@
5
5
  // tool set (minus the intentional pre-auth signup tools that only this stdio
6
6
  // package exposes). `npm run check:mcp-parity` enforces both the version match
7
7
  // and the tool-list match, and runs in the pre-commit hook.
8
- export const POSTERLY_MCP_VERSION = '0.48.3';
8
+ export const POSTERLY_MCP_VERSION = '0.48.5';
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
- import { formatDate, mdEmpty, mdTable, mdTitle } from '../lib/format.js';
3
+ import { formatDate, mdEmpty, mdTable, mdTitle, starGlyphs } from '../lib/format.js';
4
4
 
5
5
  const SUGGESTED_BASIS_LABELS: Record<string, string> = {
6
6
  yours: 'Based on your posts',
@@ -11,7 +11,7 @@ const SUGGESTED_BASIS_LABELS: Record<string, string> = {
11
11
  export const findSlotTool = {
12
12
  name: 'find_available_slot',
13
13
  description:
14
- 'Find available time slots for posting. Respects a 1-hour gap between posts and preferred hours (8am-10pm in the given timezone). Returns up to 10 slots. IMPORTANT: pass a timezone explicitly - default is America/New_York and slots will be off if the user is elsewhere. Pass workspace_id to only avoid collisions with posts in that workspace. strategy "best_engagement" (needs account_ids) ranks the next 7 days by when these accounts get the most engagement, with a reason per slot.',
14
+ 'Find available time slots for posting. Respects a 1-hour gap between posts and preferred hours (8am-10pm in the given timezone). Returns up to 10 slots. IMPORTANT: pass a timezone explicitly - default is America/New_York and slots will be off if the user is elsewhere. Pass workspace_id to only avoid collisions with posts in that workspace. strategy "best_engagement" (needs account_ids) ranks the next 7 days by when these accounts get the most engagement, with a 1-5 star rating and a reason per slot.',
15
15
  inputSchema: z.object({
16
16
  account_ids: z
17
17
  .array(z.string())
@@ -69,10 +69,11 @@ export const findSlotTool = {
69
69
  // local_time already carries the date in the requested timezone, so
70
70
  // no separate Date column (a machine-local date could disagree).
71
71
  mdTable(
72
- ['Option', 'Local time', 'Why'],
72
+ ['Option', 'Local time', 'Rating', 'Why'],
73
73
  slots.map((slot, index) => [
74
74
  index + 1,
75
75
  slot.local_time,
76
+ starGlyphs(slot.stars, slot.basis === 'default'),
76
77
  slot.reason || 'n/a',
77
78
  ]),
78
79
  ),
@@ -0,0 +1,98 @@
1
+ import { z } from 'zod';
2
+ import type {
3
+ PosterlyClient,
4
+ PublishingPauseChangeResponse,
5
+ PublishingPauseState,
6
+ PublishMissedResponse,
7
+ } from '../lib/api-client.js';
8
+
9
+ // Plain-text output shared by list_publishing_pauses and
10
+ // manage_publishing_pause. The hosted endpoint (lib/mcp/http-tools.ts) renders
11
+ // the same text from the same API responses; scripts/test-publishing-pause-api.ts
12
+ // checks the two surfaces match.
13
+
14
+ function yesNo(value: unknown): string {
15
+ return value ? 'yes' : 'no';
16
+ }
17
+
18
+ function idList(ids: number[] | undefined): string {
19
+ return Array.isArray(ids) && ids.length > 0 ? ids.join(', ') : 'none';
20
+ }
21
+
22
+ function scopeText(data: { scope?: string; workspace_id?: string; brand_id?: string | null; brand_name?: string | null; workspace_name?: string | null }): string {
23
+ if (data.scope === 'brand') return `brand ${data.brand_name ? `${data.brand_name} ` : ''}(${data.brand_id})`;
24
+ return `the workspace ${data.workspace_name ? `${data.workspace_name} ` : ''}(${data.workspace_id})`;
25
+ }
26
+
27
+ export function formatPublishingPauseState(data: PublishingPauseState): string {
28
+ const workspace = data.workspace;
29
+ const brands = Array.isArray(data.brands) ? data.brands : [];
30
+ const viewer = data.viewer;
31
+ return [
32
+ `Publishing pause for ${workspace.name || 'this workspace'} (${workspace.id})`,
33
+ `Workspace: ${workspace.paused ? `paused since ${workspace.paused_at}` : 'publishing normally'}`,
34
+ brands.length > 0
35
+ ? ['Paused brands:', ...brands.map((brand) => `- ${brand.name || 'Unnamed brand'} (${brand.id}), paused since ${brand.paused_at}`)].join('\n')
36
+ : 'Paused brands: none',
37
+ `Your role: ${viewer.role}. Pause the workspace: ${yesNo(viewer.can_pause_workspace)}. Pause a brand: ${yesNo(viewer.can_pause_brand)}. Resume and publish missed posts: ${yesNo(viewer.can_resume)}.`,
38
+ 'While paused, nothing in that scope publishes (scheduled posts are held, Post now is refused). Pause and resume with manage_publishing_pause.',
39
+ ].join('\n');
40
+ }
41
+
42
+ export function formatPublishingPauseChange(data: PublishingPauseChangeResponse): string {
43
+ if (data.requires_confirmation) {
44
+ return [
45
+ 'Preview only: nothing has changed yet.',
46
+ data.error || '',
47
+ `Scope: ${scopeText(data)}`,
48
+ `Scheduled posts to hold: ${typeof data.scheduled_posts_to_hold === 'number' ? data.scheduled_posts_to_hold : 'unknown'}`,
49
+ `Already paused: ${yesNo(data.already_paused)}`,
50
+ 'Next step: show this to the user. Only after they agree, call manage_publishing_pause again with action=pause, the same fields, and confirm=true.',
51
+ ].filter(Boolean).join('\n');
52
+ }
53
+
54
+ if (data.action === 'pause') {
55
+ const failed = data.failed_post_ids || [];
56
+ return [
57
+ `Publishing paused for ${scopeText(data)}.`,
58
+ `Held posts: ${data.held_count ?? 0}`,
59
+ failed.length > 0 ? `Posts that could not be marked as held (the publish guard still holds them): ${idList(failed)}` : '',
60
+ data.message || '',
61
+ ].filter(Boolean).join('\n');
62
+ }
63
+
64
+ const missed = data.missed_post_ids || [];
65
+ const failed = data.failed_post_ids || [];
66
+ return [
67
+ `Publishing resumed for ${scopeText(data)}.`,
68
+ `Scheduled again: ${data.resumed_count ?? 0}`,
69
+ `Missed their time while paused (still paused): ${missed.length}${missed.length > 0 ? ` [post IDs: ${idList(missed)}]` : ''}`,
70
+ `Still held by another pause: ${data.still_held_count ?? 0}`,
71
+ (data.disconnected_count ?? 0) > 0 ? `Still paused because the account is disconnected: ${data.disconnected_count}` : '',
72
+ failed.length > 0 ? `Could not be released (try resume again): ${idList(failed)}` : '',
73
+ missed.length > 0
74
+ ? 'Next step: missed posts never publish on their own. Ask the user whether to publish them now (action=publish_missed with these post_ids and confirm=true) or reschedule them.'
75
+ : '',
76
+ ].filter(Boolean).join('\n');
77
+ }
78
+
79
+ export function formatPublishMissed(data: PublishMissedResponse): string {
80
+ return [
81
+ `Missed posts: ${data.started} publishing now, ${data.skipped} skipped.`,
82
+ data.message || '',
83
+ ].filter(Boolean).join('\n');
84
+ }
85
+
86
+ export const listPublishingPausesTool = {
87
+ name: 'list_publishing_pauses',
88
+ description:
89
+ 'Is publishing paused? Shows whether the workspace is paused, which brands are paused (only brands you can see), and whether you may pause, resume, or publish missed posts. Read-only. Change it with manage_publishing_pause.',
90
+ inputSchema: z.object({
91
+ workspace_id: z.string().optional().describe('Workspace ID from whoami. Defaults to your default workspace.'),
92
+ }),
93
+
94
+ async execute(client: PosterlyClient, input: { workspace_id?: string }) {
95
+ const state = await client.getPublishingPause(input.workspace_id ? { workspace_id: input.workspace_id } : undefined);
96
+ return formatPublishingPauseState(state);
97
+ },
98
+ };
@@ -0,0 +1,62 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { validateArgs, type ToolContext } from '../lib/tool-runtime.js';
4
+ import { formatPublishingPauseChange, formatPublishMissed } from './list-publishing-pauses.js';
5
+
6
+ /** Same wording as EMPTY_BRAND_ID_MESSAGE in lib/publish/publishing-pause-service.ts. */
7
+ const EMPTY_BRAND_ID_MESSAGE = 'brand_id is empty; omit it to target the whole workspace.';
8
+
9
+ type ManagePublishingPauseInput = {
10
+ action: 'pause' | 'resume' | 'publish_missed';
11
+ workspace_id?: string;
12
+ brand_id?: string;
13
+ post_ids?: number[];
14
+ confirm?: boolean;
15
+ };
16
+
17
+ export const managePublishingPauseTool = {
18
+ name: 'manage_publishing_pause',
19
+ description:
20
+ 'Pause or resume publishing for a whole workspace or one brand (check it first with list_publishing_pauses). Only act when the user asks; never pause on your own initiative. action=pause (optional brand_id; leave it out for the whole workspace): holds every scheduled post in that scope and blocks Post now until an admin resumes. Call it first without confirm to get a preview (scope and how many scheduled posts it holds), show it to the user, and only after they agree call again with confirm=true. Publisher or higher; a seat limited to some brands can only pause those brands. action=resume (optional brand_id; admin or higher): future posts are scheduled again; posts whose time passed while paused stay paused and are listed as missed. Missed posts never publish on their own. action=publish_missed (post_ids from the resume, confirm=true after the user agrees; admin or higher): publishes those missed posts now.',
21
+ inputSchema: z.object({
22
+ action: z.enum(['pause', 'resume', 'publish_missed']).describe('pause, resume, or publish_missed.'),
23
+ workspace_id: z.string().optional().describe('Workspace ID from whoami. Defaults to your default workspace.'),
24
+ brand_id: z.string().optional().describe('pause and resume: a brand ID from list_brands. Leave it out (never empty) for the whole workspace.'),
25
+ post_ids: z.array(z.number().int().positive()).max(500).optional().describe('publish_missed: the missed post IDs a resume returned.'),
26
+ confirm: z.boolean().optional().describe('pause: leave out to get a preview; true only after the user agreed. publish_missed: true after the user agreed. resume: not needed.'),
27
+ }),
28
+
29
+ async execute(client: PosterlyClient, input: ManagePublishingPauseInput, ctx: ToolContext) {
30
+ // A present but empty brand_id is refused, never read as the whole
31
+ // workspace (same rule and wording as the API and the hosted endpoint).
32
+ if ((input?.action === 'pause' || input?.action === 'resume') && input && Object.prototype.hasOwnProperty.call(input, 'brand_id')) {
33
+ const value = (input as { brand_id?: unknown }).brand_id;
34
+ if (value === null || (typeof value === 'string' && value.trim() === '')) throw new Error(EMPTY_BRAND_ID_MESSAGE);
35
+ }
36
+ const { mode, args } = validateArgs(ctx, 'manage_publishing_pause', input);
37
+ const call = args as ManagePublishingPauseInput;
38
+ const workspace = call.workspace_id ? { workspace_id: call.workspace_id } : {};
39
+
40
+ if (mode === 'publish_missed') {
41
+ if (call.confirm !== true) {
42
+ throw new Error('Refusing to publish missed posts without confirm=true after explicit user confirmation.');
43
+ }
44
+ const result = await client.publishMissedPausedPosts({ ...workspace, post_ids: call.post_ids || [], confirm: true });
45
+ return formatPublishMissed(result);
46
+ }
47
+
48
+ const brand = call.brand_id ? { brand_id: call.brand_id } : {};
49
+ if (mode === 'pause') {
50
+ const result = await client.changePublishingPause({
51
+ action: 'pause',
52
+ ...workspace,
53
+ ...brand,
54
+ ...(call.confirm === true ? { confirm: true } : {}),
55
+ });
56
+ return formatPublishingPauseChange(result);
57
+ }
58
+
59
+ const result = await client.changePublishingPause({ action: 'resume', ...workspace, ...brand });
60
+ return formatPublishingPauseChange(result);
61
+ },
62
+ };
@@ -8,6 +8,8 @@
8
8
  "delete_api_key",
9
9
  "list_workspace_members",
10
10
  "manage_workspace_member",
11
+ "list_publishing_pauses",
12
+ "manage_publishing_pause",
11
13
  "list_accounts",
12
14
  "disconnect_account",
13
15
  "connect_account",
@@ -68,6 +70,7 @@
68
70
  "get_signup_session",
69
71
  "whoami",
70
72
  "list_workspace_members",
73
+ "list_publishing_pauses",
71
74
  "list_accounts",
72
75
  "list_oauth_clients",
73
76
  "list_platforms",
@@ -108,6 +111,7 @@
108
111
  "manage_comment",
109
112
  "manage_webhook",
110
113
  "cancel_subscription",
114
+ "manage_publishing_pause",
111
115
  "pause_subscription",
112
116
  "downgrade_subscription"
113
117
  ],
@@ -128,6 +132,7 @@
128
132
  "get_signup_session",
129
133
  "whoami",
130
134
  "manage_workspace_member",
135
+ "manage_publishing_pause",
131
136
  "list_accounts",
132
137
  "disconnect_account",
133
138
  "connect_account",
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "admin": {
28
28
  "label": "Admin",
29
- "description": "API keys, OAuth clients, webhooks, and workspace members."
29
+ "description": "API keys, OAuth clients, webhooks, workspace members, and the publishing pause."
30
30
  },
31
31
  "billing": {
32
32
  "label": "Billing",
@@ -175,6 +175,68 @@
175
175
  }
176
176
  }
177
177
  },
178
+ "list_publishing_pauses": {
179
+ "set": "admin",
180
+ "label": "Publishing pause",
181
+ "readOnly": true,
182
+ "destructive": false,
183
+ "idempotent": true,
184
+ "openWorld": false,
185
+ "confirm": "none"
186
+ },
187
+ "manage_publishing_pause": {
188
+ "set": "admin",
189
+ "label": "Pause or resume publishing",
190
+ "selector": "action",
191
+ "actions": {
192
+ "pause": {
193
+ "label": "Pause publishing",
194
+ "destructive": true,
195
+ "idempotent": true,
196
+ "openWorld": false,
197
+ "confirm": "confirm",
198
+ "params": {
199
+ "workspace_id": {
200
+ "type": "string"
201
+ },
202
+ "brand_id": {
203
+ "type": "string"
204
+ }
205
+ }
206
+ },
207
+ "resume": {
208
+ "label": "Resume publishing",
209
+ "destructive": false,
210
+ "idempotent": true,
211
+ "openWorld": false,
212
+ "confirm": "none",
213
+ "params": {
214
+ "workspace_id": {
215
+ "type": "string"
216
+ },
217
+ "brand_id": {
218
+ "type": "string"
219
+ }
220
+ }
221
+ },
222
+ "publish_missed": {
223
+ "label": "Publish missed posts now",
224
+ "destructive": false,
225
+ "idempotent": false,
226
+ "openWorld": true,
227
+ "confirm": "confirm",
228
+ "params": {
229
+ "workspace_id": {
230
+ "type": "string"
231
+ },
232
+ "post_ids": {
233
+ "type": "array",
234
+ "required": true
235
+ }
236
+ }
237
+ }
238
+ }
239
+ },
178
240
  "list_accounts": {
179
241
  "set": "core",
180
242
  "label": "Connected accounts",