posterly-mcp-server 0.48.2 → 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.2` 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
 
@@ -555,6 +555,17 @@ export interface Slot {
555
555
  time: string;
556
556
  local_time: string;
557
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
+ }
558
569
  export interface Workspace {
559
570
  id: string;
560
571
  name: string;
@@ -1364,6 +1375,13 @@ export declare class PosterlyClient {
1364
1375
  count?: number;
1365
1376
  workspace_id?: string;
1366
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>;
1367
1385
  getAccountAnalytics(params: {
1368
1386
  account_id: number;
1369
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.2";
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.2';
8
+ export const POSTERLY_MCP_VERSION = '0.48.3';
@@ -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.2",
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.2",
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.2",
17
+ "version": "0.48.3",
18
18
  "transport": {
19
19
  "type": "stdio"
20
20
  },
@@ -606,6 +606,20 @@ export interface Slot {
606
606
  local_time: string;
607
607
  }
608
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
+
609
623
  export interface Workspace {
610
624
  id: string;
611
625
  name: string;
@@ -1788,6 +1802,21 @@ export class PosterlyClient {
1788
1802
  return data.slots;
1789
1803
  }
1790
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
+
1791
1820
  async getAccountAnalytics(params: {
1792
1821
  account_id: number;
1793
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.2';
8
+ export const POSTERLY_MCP_VERSION = '0.48.3';
@@ -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) {