posterly-mcp-server 0.48.1 → 0.48.3

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.1` 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.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.
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
 
@@ -237,6 +237,10 @@ export type CreatePostResponse = {
237
237
  is_personal: boolean;
238
238
  resolved_from: 'explicit' | 'account' | 'personal';
239
239
  };
240
+ warnings?: Array<{
241
+ code: string;
242
+ message: string;
243
+ }>;
240
244
  };
241
245
  export type ValidatePostResponse = {
242
246
  dry_run: true;
@@ -551,6 +555,17 @@ export interface Slot {
551
555
  time: string;
552
556
  local_time: string;
553
557
  }
558
+ export type SuggestedSlotBasis = 'yours' | 'platform' | 'default';
559
+ export interface SuggestedSlot extends Slot {
560
+ basis: SuggestedSlotBasis;
561
+ reason: string;
562
+ }
563
+ export interface SuggestedSlotsResponse {
564
+ timezone: string;
565
+ basis: SuggestedSlotBasis;
566
+ basis_label: string;
567
+ slots: SuggestedSlot[];
568
+ }
554
569
  export interface Workspace {
555
570
  id: string;
556
571
  name: string;
@@ -1360,6 +1375,13 @@ export declare class PosterlyClient {
1360
1375
  count?: number;
1361
1376
  workspace_id?: string;
1362
1377
  }): Promise<Slot[]>;
1378
+ /** Best times by engagement for the given accounts (GET /slots/suggested). */
1379
+ findSuggestedSlots(params: {
1380
+ account_ids: string[];
1381
+ timezone?: string;
1382
+ count?: number;
1383
+ workspace_id?: string;
1384
+ }): Promise<SuggestedSlotsResponse>;
1363
1385
  getAccountAnalytics(params: {
1364
1386
  account_id: number;
1365
1387
  from?: string;
@@ -452,6 +452,18 @@ export class PosterlyClient {
452
452
  const data = await this.request('GET', `/slots/next${qs ? `?${qs}` : ''}`);
453
453
  return data.slots;
454
454
  }
455
+ /** Best times by engagement for the given accounts (GET /slots/suggested). */
456
+ async findSuggestedSlots(params) {
457
+ const searchParams = new URLSearchParams();
458
+ searchParams.set('account_ids', params.account_ids.join(','));
459
+ if (params.timezone)
460
+ searchParams.set('timezone', params.timezone);
461
+ if (params.count)
462
+ searchParams.set('count', String(params.count));
463
+ if (params.workspace_id)
464
+ searchParams.set('workspace_id', params.workspace_id);
465
+ return this.request('GET', `/slots/suggested?${searchParams.toString()}`);
466
+ }
455
467
  async getAccountAnalytics(params) {
456
468
  const searchParams = new URLSearchParams();
457
469
  searchParams.set('account_id', String(params.account_id));
@@ -1 +1 @@
1
- export declare const POSTERLY_MCP_VERSION = "0.48.1";
1
+ export declare const POSTERLY_MCP_VERSION = "0.48.3";
@@ -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.1';
8
+ export const POSTERLY_MCP_VERSION = '0.48.3';
@@ -382,6 +382,12 @@ export const createPostTool = {
382
382
  if (ws) {
383
383
  lines.push(`• Workspace: ${ws.name} (${ws.id}) - resolved from ${ws.resolved_from}`);
384
384
  }
385
+ if (result.warnings && result.warnings.length > 0) {
386
+ lines.push('• Warnings:');
387
+ for (const warning of result.warnings) {
388
+ lines.push(` - ${warning.message || warning.code}`);
389
+ }
390
+ }
385
391
  lines.push('', 'Next step for the AI agent: tell the user the post is in posterly, share the dashboard link above, then ask whether they want to create another post, adjust this one, or connect another account.');
386
392
  return lines.join('\n');
387
393
  },
@@ -54,6 +54,12 @@ export const createPostsBatchTool = {
54
54
  for (const error of result.errors.slice(0, 25)) {
55
55
  lines.push(`• [${error.index}] ERROR ${error.status}: ${error.error}`);
56
56
  }
57
+ for (const item of result.posts.slice(0, 25)) {
58
+ const warnings = item.warnings || [];
59
+ for (const warning of warnings) {
60
+ lines.push(`• [${item.index}] WARNING ${warning.message || warning.code}`);
61
+ }
62
+ }
57
63
  lines.push('', 'Next step for the AI agent: tell the user which posts were created, share the dashboard link above, and ask whether they want to review the queue, adjust anything, or create the next post.');
58
64
  return lines.join('\n');
59
65
  },
@@ -8,21 +8,25 @@ export declare const findSlotTool: {
8
8
  timezone: z.ZodOptional<z.ZodString>;
9
9
  count: z.ZodOptional<z.ZodNumber>;
10
10
  workspace_id: z.ZodOptional<z.ZodString>;
11
+ strategy: z.ZodOptional<z.ZodEnum<["next_free", "best_engagement"]>>;
11
12
  }, "strip", z.ZodTypeAny, {
12
13
  workspace_id?: string | undefined;
13
14
  account_ids?: string[] | undefined;
14
15
  timezone?: string | undefined;
15
16
  count?: number | undefined;
17
+ strategy?: "next_free" | "best_engagement" | undefined;
16
18
  }, {
17
19
  workspace_id?: string | undefined;
18
20
  account_ids?: string[] | undefined;
19
21
  timezone?: string | undefined;
20
22
  count?: number | undefined;
23
+ strategy?: "next_free" | "best_engagement" | undefined;
21
24
  }>;
22
25
  execute(client: PosterlyClient, input: {
23
26
  account_ids?: string[];
24
27
  timezone?: string;
25
28
  count?: number;
26
29
  workspace_id?: string;
30
+ strategy?: "next_free" | "best_engagement";
27
31
  }): Promise<string>;
28
32
  };
@@ -1,8 +1,13 @@
1
1
  import { z } from 'zod';
2
2
  import { formatDate, mdEmpty, mdTable, mdTitle } from '../lib/format.js';
3
+ const SUGGESTED_BASIS_LABELS = {
4
+ yours: 'Based on your posts',
5
+ platform: 'Popular on this platform',
6
+ default: 'Good default',
7
+ };
3
8
  export const findSlotTool = {
4
9
  name: 'find_available_slot',
5
- 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.',
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.',
6
11
  inputSchema: z.object({
7
12
  account_ids: z
8
13
  .array(z.string())
@@ -17,13 +22,45 @@ export const findSlotTool = {
17
22
  .min(1)
18
23
  .max(10)
19
24
  .optional()
20
- .describe('Number of slots to return (default 5, max 10)'),
25
+ .describe('Number of slots to return (max 10). Default 5 for next_free, 3 for best_engagement.'),
21
26
  workspace_id: z
22
27
  .string()
23
28
  .optional()
24
29
  .describe('Scope slot-finding to a single workspace (get IDs via whoami).'),
30
+ strategy: z
31
+ .enum(['next_free', 'best_engagement'])
32
+ .optional()
33
+ .describe('next_free (default): earliest free slots. best_engagement: best times by engagement (requires account_ids).'),
25
34
  }),
26
35
  async execute(client, input) {
36
+ if (input.strategy === 'best_engagement') {
37
+ const accountIds = (input.account_ids || []).filter(Boolean);
38
+ if (accountIds.length === 0) {
39
+ throw new Error('strategy "best_engagement" needs account_ids (get them from list_accounts).');
40
+ }
41
+ const result = await client.findSuggestedSlots({
42
+ account_ids: accountIds,
43
+ timezone: input.timezone,
44
+ count: input.count,
45
+ workspace_id: input.workspace_id,
46
+ });
47
+ const slots = Array.isArray(result.slots) ? result.slots : [];
48
+ if (slots.length === 0) {
49
+ return mdEmpty('suggested slots', 'No suggested slots are free in the next 7 days.');
50
+ }
51
+ const basis = result.basis_label || SUGGESTED_BASIS_LABELS[result.basis] || 'Good default';
52
+ return [
53
+ mdTitle(`Best times to post (${slots.length})`),
54
+ `Basis: ${basis}${result.timezone ? ` (${result.timezone})` : ''}`,
55
+ // local_time already carries the date in the requested timezone, so
56
+ // no separate Date column (a machine-local date could disagree).
57
+ mdTable(['Option', 'Local time', 'Why'], slots.map((slot, index) => [
58
+ index + 1,
59
+ slot.local_time,
60
+ slot.reason || 'n/a',
61
+ ])),
62
+ ].join('\n\n');
63
+ }
27
64
  const slots = await client.findAvailableSlots(input);
28
65
  if (slots.length === 0) {
29
66
  return mdEmpty('available slots', 'No available posting slots were found in the next 14 days.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posterly-mcp-server",
3
- "version": "0.48.1",
3
+ "version": "0.48.3",
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.1",
6
+ "version": "0.48.3",
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.1",
17
+ "version": "0.48.3",
18
18
  "transport": {
19
19
  "type": "stdio"
20
20
  },
@@ -269,6 +269,7 @@ export type CreatePostPayload = {
269
269
  export type CreatePostResponse = {
270
270
  post: Post;
271
271
  workspace: { id: string; name: string; is_personal: boolean; resolved_from: 'explicit' | 'account' | 'personal' };
272
+ warnings?: Array<{ code: string; message: string }>;
272
273
  };
273
274
 
274
275
  export type ValidatePostResponse = {
@@ -605,6 +606,20 @@ export interface Slot {
605
606
  local_time: string;
606
607
  }
607
608
 
609
+ export type SuggestedSlotBasis = 'yours' | 'platform' | 'default';
610
+
611
+ export interface SuggestedSlot extends Slot {
612
+ basis: SuggestedSlotBasis;
613
+ reason: string;
614
+ }
615
+
616
+ export interface SuggestedSlotsResponse {
617
+ timezone: string;
618
+ basis: SuggestedSlotBasis;
619
+ basis_label: string;
620
+ slots: SuggestedSlot[];
621
+ }
622
+
608
623
  export interface Workspace {
609
624
  id: string;
610
625
  name: string;
@@ -1787,6 +1802,21 @@ export class PosterlyClient {
1787
1802
  return data.slots;
1788
1803
  }
1789
1804
 
1805
+ /** Best times by engagement for the given accounts (GET /slots/suggested). */
1806
+ async findSuggestedSlots(params: {
1807
+ account_ids: string[];
1808
+ timezone?: string;
1809
+ count?: number;
1810
+ workspace_id?: string;
1811
+ }): Promise<SuggestedSlotsResponse> {
1812
+ const searchParams = new URLSearchParams();
1813
+ searchParams.set('account_ids', params.account_ids.join(','));
1814
+ if (params.timezone) searchParams.set('timezone', params.timezone);
1815
+ if (params.count) searchParams.set('count', String(params.count));
1816
+ if (params.workspace_id) searchParams.set('workspace_id', params.workspace_id);
1817
+ return this.request<SuggestedSlotsResponse>('GET', `/slots/suggested?${searchParams.toString()}`);
1818
+ }
1819
+
1790
1820
  async getAccountAnalytics(params: {
1791
1821
  account_id: number;
1792
1822
  from?: string;
@@ -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.1';
8
+ export const POSTERLY_MCP_VERSION = '0.48.3';
@@ -415,6 +415,12 @@ export const createPostTool = {
415
415
  if (ws) {
416
416
  lines.push(`• Workspace: ${ws.name} (${ws.id}) - resolved from ${ws.resolved_from}`);
417
417
  }
418
+ if (result.warnings && result.warnings.length > 0) {
419
+ lines.push('• Warnings:');
420
+ for (const warning of result.warnings) {
421
+ lines.push(` - ${warning.message || warning.code}`);
422
+ }
423
+ }
418
424
  lines.push(
419
425
  '',
420
426
  'Next step for the AI agent: tell the user the post is in posterly, share the dashboard link above, then ask whether they want to create another post, adjust this one, or connect another account.',
@@ -63,6 +63,13 @@ export const createPostsBatchTool = {
63
63
  lines.push(`• [${error.index}] ERROR ${error.status}: ${error.error}`);
64
64
  }
65
65
 
66
+ for (const item of result.posts.slice(0, 25)) {
67
+ const warnings = item.warnings || [];
68
+ for (const warning of warnings) {
69
+ lines.push(`• [${item.index}] WARNING ${warning.message || warning.code}`);
70
+ }
71
+ }
72
+
66
73
  lines.push(
67
74
  '',
68
75
  'Next step for the AI agent: tell the user which posts were created, share the dashboard link above, and ask whether they want to review the queue, adjust anything, or create the next post.',
@@ -2,10 +2,16 @@ import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
3
  import { formatDate, mdEmpty, mdTable, mdTitle } from '../lib/format.js';
4
4
 
5
+ const SUGGESTED_BASIS_LABELS: Record<string, string> = {
6
+ yours: 'Based on your posts',
7
+ platform: 'Popular on this platform',
8
+ default: 'Good default',
9
+ };
10
+
5
11
  export const findSlotTool = {
6
12
  name: 'find_available_slot',
7
13
  description:
8
- '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.',
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.',
9
15
  inputSchema: z.object({
10
16
  account_ids: z
11
17
  .array(z.string())
@@ -20,11 +26,15 @@ export const findSlotTool = {
20
26
  .min(1)
21
27
  .max(10)
22
28
  .optional()
23
- .describe('Number of slots to return (default 5, max 10)'),
29
+ .describe('Number of slots to return (max 10). Default 5 for next_free, 3 for best_engagement.'),
24
30
  workspace_id: z
25
31
  .string()
26
32
  .optional()
27
33
  .describe('Scope slot-finding to a single workspace (get IDs via whoami).'),
34
+ strategy: z
35
+ .enum(['next_free', 'best_engagement'])
36
+ .optional()
37
+ .describe('next_free (default): earliest free slots. best_engagement: best times by engagement (requires account_ids).'),
28
38
  }),
29
39
 
30
40
  async execute(
@@ -34,8 +44,41 @@ export const findSlotTool = {
34
44
  timezone?: string;
35
45
  count?: number;
36
46
  workspace_id?: string;
47
+ strategy?: 'next_free' | 'best_engagement';
37
48
  }
38
49
  ) {
50
+ if (input.strategy === 'best_engagement') {
51
+ const accountIds = (input.account_ids || []).filter(Boolean);
52
+ if (accountIds.length === 0) {
53
+ throw new Error('strategy "best_engagement" needs account_ids (get them from list_accounts).');
54
+ }
55
+ const result = await client.findSuggestedSlots({
56
+ account_ids: accountIds,
57
+ timezone: input.timezone,
58
+ count: input.count,
59
+ workspace_id: input.workspace_id,
60
+ });
61
+ const slots = Array.isArray(result.slots) ? result.slots : [];
62
+ if (slots.length === 0) {
63
+ return mdEmpty('suggested slots', 'No suggested slots are free in the next 7 days.');
64
+ }
65
+ const basis = result.basis_label || SUGGESTED_BASIS_LABELS[result.basis] || 'Good default';
66
+ return [
67
+ mdTitle(`Best times to post (${slots.length})`),
68
+ `Basis: ${basis}${result.timezone ? ` (${result.timezone})` : ''}`,
69
+ // local_time already carries the date in the requested timezone, so
70
+ // no separate Date column (a machine-local date could disagree).
71
+ mdTable(
72
+ ['Option', 'Local time', 'Why'],
73
+ slots.map((slot, index) => [
74
+ index + 1,
75
+ slot.local_time,
76
+ slot.reason || 'n/a',
77
+ ]),
78
+ ),
79
+ ].join('\n\n');
80
+ }
81
+
39
82
  const slots = await client.findAvailableSlots(input);
40
83
 
41
84
  if (slots.length === 0) {