posterly-mcp-server 0.48.5 → 0.49.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 +8 -2
- package/dist/lib/api-client.d.ts +80 -12
- package/dist/lib/api-client.js +40 -15
- package/dist/lib/format.d.ts +39 -2
- package/dist/lib/format.js +158 -0
- package/dist/lib/version.d.ts +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/tools/audit-google-business-profile.d.ts +2 -2
- package/dist/tools/connect-account.d.ts +2 -2
- package/dist/tools/create-post.d.ts +12 -12
- package/dist/tools/create-posts-batch.d.ts +8 -8
- package/dist/tools/create-webhook.d.ts +2 -2
- package/dist/tools/disconnect-account.d.ts +2 -2
- package/dist/tools/find-slot.d.ts +2 -2
- package/dist/tools/get-updates.d.ts +4 -0
- package/dist/tools/get-updates.js +13 -1
- package/dist/tools/list-accounts.d.ts +2 -2
- package/dist/tools/list-activity.d.ts +4 -0
- package/dist/tools/list-activity.js +16 -3
- package/dist/tools/list-analytics.d.ts +2 -2
- package/dist/tools/list-comments.d.ts +2 -2
- package/dist/tools/list-conversations.d.ts +2 -2
- package/dist/tools/list-google-business-media.d.ts +2 -2
- package/dist/tools/list-google-business-reviews.d.ts +4 -4
- package/dist/tools/list-media.d.ts +4 -0
- package/dist/tools/list-media.js +22 -1
- package/dist/tools/list-platforms.d.ts +4 -4
- package/dist/tools/list-post-suggestions.d.ts +4 -0
- package/dist/tools/list-post-suggestions.js +16 -4
- package/dist/tools/list-posts.d.ts +35 -17
- package/dist/tools/list-posts.js +63 -33
- package/dist/tools/manage-comment.d.ts +2 -2
- package/dist/tools/manage-conversation.d.ts +4 -4
- package/dist/tools/manage-google-business-media.d.ts +8 -8
- package/dist/tools/manage-google-business-review.d.ts +10 -10
- package/dist/tools/manage-publishing-pause.d.ts +2 -2
- package/dist/tools/manage-webhook.d.ts +4 -4
- package/dist/tools/validate-post.d.ts +8 -8
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/lib/api-client.ts +94 -17
- package/src/lib/format.ts +201 -1
- package/src/lib/version.ts +1 -1
- package/src/tools/get-updates.ts +15 -1
- package/src/tools/list-activity.ts +18 -3
- package/src/tools/list-media.ts +23 -2
- package/src/tools/list-post-suggestions.ts +17 -4
- package/src/tools/list-posts.ts +74 -45
- package/tool-registry.json +110 -5
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.
|
|
126
|
+
`posterly-mcp-server@0.49.0` exposes 64 tools covering 100 actions. The hosted HTTP endpoint exposes the same catalog minus the three signup tools: 61 tools covering 97 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
|
|
|
@@ -147,7 +147,6 @@ Accounts, posting, media, schedule, analytics, and support. What most assistants
|
|
|
147
147
|
| `connect_account` | Connect with credentials |
|
|
148
148
|
| `create_connect_session` | Start a connect session |
|
|
149
149
|
| `trigger_platform_helper` | Run a platform helper |
|
|
150
|
-
| `list_posts` | Posts |
|
|
151
150
|
| `ask_support` | Ask support |
|
|
152
151
|
| `find_available_slot` | Find a free slot |
|
|
153
152
|
| `submit_agent_feedback` | Agent feedback |
|
|
@@ -197,6 +196,13 @@ Accounts, posting, media, schedule, analytics, and support. What most assistants
|
|
|
197
196
|
| `accounts` | Brand accounts |
|
|
198
197
|
| `profile` | Brand profile |
|
|
199
198
|
|
|
199
|
+
**`list_posts`**: Posts
|
|
200
|
+
|
|
201
|
+
| View | What it does |
|
|
202
|
+
| --- | --- |
|
|
203
|
+
| `list` (default) | Posts, one page at a time |
|
|
204
|
+
| `counts` | Post counts by day, status, account, and brand |
|
|
205
|
+
|
|
200
206
|
**`get_post`**: One post
|
|
201
207
|
|
|
202
208
|
| View | What it does |
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -134,6 +134,11 @@ export interface PostSuggestion {
|
|
|
134
134
|
}
|
|
135
135
|
export interface PostSuggestionsResponse {
|
|
136
136
|
suggestions: PostSuggestion[];
|
|
137
|
+
total?: number;
|
|
138
|
+
limit?: number;
|
|
139
|
+
offset?: number;
|
|
140
|
+
has_more?: boolean;
|
|
141
|
+
next_offset?: number | null;
|
|
137
142
|
}
|
|
138
143
|
export interface DismissSuggestionResponse {
|
|
139
144
|
ok: boolean;
|
|
@@ -169,6 +174,59 @@ export interface Post {
|
|
|
169
174
|
content_plan_id?: string | null;
|
|
170
175
|
approval?: ApiPostApproval;
|
|
171
176
|
}
|
|
177
|
+
/** Filters shared by GET /posts and GET /posts/counts. */
|
|
178
|
+
export interface ListPostsParams {
|
|
179
|
+
status?: string;
|
|
180
|
+
platform?: string;
|
|
181
|
+
account_id?: string;
|
|
182
|
+
workspace_id?: string;
|
|
183
|
+
approval_status?: string;
|
|
184
|
+
approval_waiting_on?: string;
|
|
185
|
+
brand_id?: string;
|
|
186
|
+
scheduled_from?: string;
|
|
187
|
+
scheduled_to?: string;
|
|
188
|
+
timezone?: string;
|
|
189
|
+
}
|
|
190
|
+
export interface PostsRangeEcho {
|
|
191
|
+
from: string | null;
|
|
192
|
+
to: string | null;
|
|
193
|
+
timezone: string;
|
|
194
|
+
}
|
|
195
|
+
export interface ListPostsResponse {
|
|
196
|
+
posts: Post[];
|
|
197
|
+
total: number;
|
|
198
|
+
limit?: number;
|
|
199
|
+
offset?: number;
|
|
200
|
+
has_more?: boolean;
|
|
201
|
+
next_offset?: number | null;
|
|
202
|
+
order?: 'asc' | 'desc';
|
|
203
|
+
range?: PostsRangeEcho | null;
|
|
204
|
+
}
|
|
205
|
+
export interface PostCountsResponse {
|
|
206
|
+
total: number;
|
|
207
|
+
counted: number;
|
|
208
|
+
truncated: boolean;
|
|
209
|
+
range: PostsRangeEcho | null;
|
|
210
|
+
by_day: Array<{
|
|
211
|
+
date: string;
|
|
212
|
+
count: number;
|
|
213
|
+
}>;
|
|
214
|
+
by_status: Array<{
|
|
215
|
+
status: string;
|
|
216
|
+
count: number;
|
|
217
|
+
}>;
|
|
218
|
+
by_account: Array<{
|
|
219
|
+
account_id: string | null;
|
|
220
|
+
platform: string | null;
|
|
221
|
+
username: string | null;
|
|
222
|
+
count: number;
|
|
223
|
+
}>;
|
|
224
|
+
by_brand: Array<{
|
|
225
|
+
brand_id: string | null;
|
|
226
|
+
name: string | null;
|
|
227
|
+
count: number;
|
|
228
|
+
}>;
|
|
229
|
+
}
|
|
172
230
|
export interface InstagramPostSettings {
|
|
173
231
|
__type?: 'instagram' | 'instagram-standalone';
|
|
174
232
|
post_type?: 'post' | 'feed' | 'story' | 'reel' | 'carousel';
|
|
@@ -891,8 +949,12 @@ export interface ActivityItem {
|
|
|
891
949
|
}
|
|
892
950
|
export interface ActivityResponse {
|
|
893
951
|
items: ActivityItem[];
|
|
952
|
+
/** Items on this page (kept for older clients); use has_more to tell whether more exist. */
|
|
894
953
|
total: number;
|
|
895
954
|
limit: number;
|
|
955
|
+
offset?: number;
|
|
956
|
+
has_more?: boolean;
|
|
957
|
+
next_offset?: number | null;
|
|
896
958
|
}
|
|
897
959
|
export interface UpdateEntry {
|
|
898
960
|
slug: string;
|
|
@@ -909,6 +971,11 @@ export interface UpdateEntry {
|
|
|
909
971
|
}
|
|
910
972
|
export interface UpdatesResponse {
|
|
911
973
|
updates: UpdateEntry[];
|
|
974
|
+
total?: number;
|
|
975
|
+
limit?: number;
|
|
976
|
+
offset?: number;
|
|
977
|
+
has_more?: boolean;
|
|
978
|
+
next_offset?: number | null;
|
|
912
979
|
}
|
|
913
980
|
export interface ApiWebhook {
|
|
914
981
|
id: string;
|
|
@@ -1316,22 +1383,16 @@ export declare class PosterlyClient {
|
|
|
1316
1383
|
account_id?: string | number;
|
|
1317
1384
|
status?: 'pending' | 'scheduled' | 'dismissed';
|
|
1318
1385
|
limit?: number;
|
|
1386
|
+
offset?: number;
|
|
1319
1387
|
}): Promise<PostSuggestionsResponse>;
|
|
1320
1388
|
dismissSuggestion(id: string): Promise<DismissSuggestionResponse>;
|
|
1321
|
-
listPosts(params?: {
|
|
1322
|
-
status?: string;
|
|
1323
|
-
platform?: string;
|
|
1324
|
-
account_id?: string;
|
|
1389
|
+
listPosts(params?: ListPostsParams & {
|
|
1325
1390
|
limit?: number;
|
|
1326
1391
|
offset?: number;
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
}): Promise<{
|
|
1332
|
-
posts: Post[];
|
|
1333
|
-
total: number;
|
|
1334
|
-
}>;
|
|
1392
|
+
order?: 'asc' | 'desc';
|
|
1393
|
+
}): Promise<ListPostsResponse>;
|
|
1394
|
+
/** GET /posts/counts: exact totals per day, status, account, and brand for a scheduled range. */
|
|
1395
|
+
countPosts(params: ListPostsParams): Promise<PostCountsResponse>;
|
|
1335
1396
|
createPost(data: CreatePostPayload): Promise<CreatePostResponse>;
|
|
1336
1397
|
validatePost(data: CreatePostPayload): Promise<ValidatePostResponse>;
|
|
1337
1398
|
submitAgentFeedback(data: AgentFeedbackPayload): Promise<AgentFeedbackResponse>;
|
|
@@ -1543,9 +1604,11 @@ export declare class PosterlyClient {
|
|
|
1543
1604
|
since?: string;
|
|
1544
1605
|
type?: 'all' | 'activity' | 'publish';
|
|
1545
1606
|
limit?: number;
|
|
1607
|
+
offset?: number;
|
|
1546
1608
|
}): Promise<ActivityResponse>;
|
|
1547
1609
|
getUpdates(params?: {
|
|
1548
1610
|
limit?: number;
|
|
1611
|
+
offset?: number;
|
|
1549
1612
|
since?: string;
|
|
1550
1613
|
include_content?: boolean;
|
|
1551
1614
|
}): Promise<UpdatesResponse>;
|
|
@@ -1624,6 +1687,7 @@ export declare class PosterlyClient {
|
|
|
1624
1687
|
}>;
|
|
1625
1688
|
listMedia(params?: {
|
|
1626
1689
|
limit?: number;
|
|
1690
|
+
offset?: number;
|
|
1627
1691
|
dropSessionId?: string;
|
|
1628
1692
|
}): Promise<{
|
|
1629
1693
|
media: Array<{
|
|
@@ -1637,6 +1701,10 @@ export declare class PosterlyClient {
|
|
|
1637
1701
|
drop_session_id: string | null;
|
|
1638
1702
|
}>;
|
|
1639
1703
|
total: number;
|
|
1704
|
+
limit?: number;
|
|
1705
|
+
offset?: number;
|
|
1706
|
+
has_more?: boolean;
|
|
1707
|
+
next_offset?: number | null;
|
|
1640
1708
|
}>;
|
|
1641
1709
|
uploadMedia(input: {
|
|
1642
1710
|
filePath?: string;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -36,6 +36,30 @@ export function setMcpClientNameResolver(resolver) {
|
|
|
36
36
|
export function sanitizeClientHeaderValue(value) {
|
|
37
37
|
return value.replace(/[^\x20-\x7e]/g, '').trim().slice(0, 200);
|
|
38
38
|
}
|
|
39
|
+
function postFilterParams(params) {
|
|
40
|
+
const searchParams = new URLSearchParams();
|
|
41
|
+
if (params?.status)
|
|
42
|
+
searchParams.set('status', params.status);
|
|
43
|
+
if (params?.platform)
|
|
44
|
+
searchParams.set('platform', params.platform);
|
|
45
|
+
if (params?.account_id)
|
|
46
|
+
searchParams.set('account_id', params.account_id);
|
|
47
|
+
if (params?.workspace_id)
|
|
48
|
+
searchParams.set('workspace_id', params.workspace_id);
|
|
49
|
+
if (params?.approval_status)
|
|
50
|
+
searchParams.set('approval_status', params.approval_status);
|
|
51
|
+
if (params?.approval_waiting_on)
|
|
52
|
+
searchParams.set('approval_waiting_on', params.approval_waiting_on);
|
|
53
|
+
if (params?.brand_id)
|
|
54
|
+
searchParams.set('brand_id', params.brand_id);
|
|
55
|
+
if (params?.scheduled_from)
|
|
56
|
+
searchParams.set('scheduled_from', params.scheduled_from);
|
|
57
|
+
if (params?.scheduled_to)
|
|
58
|
+
searchParams.set('scheduled_to', params.scheduled_to);
|
|
59
|
+
if (params?.timezone)
|
|
60
|
+
searchParams.set('timezone', params.timezone);
|
|
61
|
+
return searchParams;
|
|
62
|
+
}
|
|
39
63
|
export class PosterlyClient {
|
|
40
64
|
baseUrl;
|
|
41
65
|
apiKey;
|
|
@@ -336,6 +360,8 @@ export class PosterlyClient {
|
|
|
336
360
|
searchParams.set('status', params.status);
|
|
337
361
|
if (params?.limit)
|
|
338
362
|
searchParams.set('limit', String(params.limit));
|
|
363
|
+
if (params?.offset)
|
|
364
|
+
searchParams.set('offset', String(params.offset));
|
|
339
365
|
const qs = searchParams.toString();
|
|
340
366
|
return this.request('GET', `/suggestions${qs ? `?${qs}` : ''}`);
|
|
341
367
|
}
|
|
@@ -343,28 +369,21 @@ export class PosterlyClient {
|
|
|
343
369
|
return this.request('POST', `/suggestions/${encodeURIComponent(id)}/dismiss`, {});
|
|
344
370
|
}
|
|
345
371
|
async listPosts(params) {
|
|
346
|
-
const searchParams =
|
|
347
|
-
if (params?.status)
|
|
348
|
-
searchParams.set('status', params.status);
|
|
349
|
-
if (params?.platform)
|
|
350
|
-
searchParams.set('platform', params.platform);
|
|
351
|
-
if (params?.account_id)
|
|
352
|
-
searchParams.set('account_id', params.account_id);
|
|
372
|
+
const searchParams = postFilterParams(params);
|
|
353
373
|
if (params?.limit)
|
|
354
374
|
searchParams.set('limit', String(params.limit));
|
|
355
375
|
if (params?.offset)
|
|
356
376
|
searchParams.set('offset', String(params.offset));
|
|
357
|
-
if (params?.
|
|
358
|
-
searchParams.set('
|
|
359
|
-
if (params?.approval_status)
|
|
360
|
-
searchParams.set('approval_status', params.approval_status);
|
|
361
|
-
if (params?.approval_waiting_on)
|
|
362
|
-
searchParams.set('approval_waiting_on', params.approval_waiting_on);
|
|
363
|
-
if (params?.brand_id)
|
|
364
|
-
searchParams.set('brand_id', params.brand_id);
|
|
377
|
+
if (params?.order)
|
|
378
|
+
searchParams.set('order', params.order);
|
|
365
379
|
const qs = searchParams.toString();
|
|
366
380
|
return this.request('GET', `/posts${qs ? `?${qs}` : ''}`);
|
|
367
381
|
}
|
|
382
|
+
/** GET /posts/counts: exact totals per day, status, account, and brand for a scheduled range. */
|
|
383
|
+
async countPosts(params) {
|
|
384
|
+
const qs = postFilterParams(params).toString();
|
|
385
|
+
return this.request('GET', `/posts/counts${qs ? `?${qs}` : ''}`);
|
|
386
|
+
}
|
|
368
387
|
async createPost(data) {
|
|
369
388
|
return this.request('POST', '/posts', data);
|
|
370
389
|
}
|
|
@@ -581,6 +600,8 @@ export class PosterlyClient {
|
|
|
581
600
|
searchParams.set('type', params.type);
|
|
582
601
|
if (params?.limit)
|
|
583
602
|
searchParams.set('limit', String(params.limit));
|
|
603
|
+
if (params?.offset)
|
|
604
|
+
searchParams.set('offset', String(params.offset));
|
|
584
605
|
const qs = searchParams.toString();
|
|
585
606
|
return this.request('GET', `/activity${qs ? `?${qs}` : ''}`);
|
|
586
607
|
}
|
|
@@ -588,6 +609,8 @@ export class PosterlyClient {
|
|
|
588
609
|
const searchParams = new URLSearchParams();
|
|
589
610
|
if (params?.limit)
|
|
590
611
|
searchParams.set('limit', String(params.limit));
|
|
612
|
+
if (params?.offset)
|
|
613
|
+
searchParams.set('offset', String(params.offset));
|
|
591
614
|
if (params?.since)
|
|
592
615
|
searchParams.set('since', params.since);
|
|
593
616
|
if (params?.include_content)
|
|
@@ -661,6 +684,8 @@ export class PosterlyClient {
|
|
|
661
684
|
const searchParams = new URLSearchParams();
|
|
662
685
|
if (params?.limit)
|
|
663
686
|
searchParams.set('limit', String(params.limit));
|
|
687
|
+
if (params?.offset)
|
|
688
|
+
searchParams.set('offset', String(params.offset));
|
|
664
689
|
if (params?.dropSessionId)
|
|
665
690
|
searchParams.set('drop_session_id', params.dropSessionId);
|
|
666
691
|
const qs = searchParams.toString();
|
package/dist/lib/format.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Account, LearnedVoice, Post } from './api-client.js';
|
|
1
|
+
import type { Account, LearnedVoice, ListPostsResponse, Post, PostCountsResponse, PostsRangeEcho } from './api-client.js';
|
|
2
2
|
type CellValue = string | number | boolean | null | undefined;
|
|
3
3
|
export declare const DASHBOARD_CALENDAR_URL: string;
|
|
4
4
|
export declare const DASHBOARD_TABLE_URL: string;
|
|
@@ -41,4 +41,41 @@ export declare function compactText(value: string | null | undefined, maxLength?
|
|
|
41
41
|
export declare function mdLearnedVoice(learned: LearnedVoice | null | undefined): string | null;
|
|
42
42
|
export declare function statusLabel(status: string | null | undefined): string;
|
|
43
43
|
export declare function code(value: CellValue): string;
|
|
44
|
-
|
|
44
|
+
/**
|
|
45
|
+
* The completeness line every paged list prints, so an agent always knows
|
|
46
|
+
* whether it has seen everything. Shared by the npm package and the hosted
|
|
47
|
+
* endpoint.
|
|
48
|
+
*/
|
|
49
|
+
export declare function formatPagingNote(input: {
|
|
50
|
+
noun: string;
|
|
51
|
+
tool: string;
|
|
52
|
+
offset?: number | null;
|
|
53
|
+
count: number;
|
|
54
|
+
total?: number | null;
|
|
55
|
+
hasMore: boolean;
|
|
56
|
+
nextOffset?: number | null;
|
|
57
|
+
}): string;
|
|
58
|
+
export type PostListRange = PostsRangeEcho;
|
|
59
|
+
export type PostListPage = ListPostsResponse;
|
|
60
|
+
export type { PostCountsResponse };
|
|
61
|
+
/** "Wed, Oct 1, 2026, 09:00" in `timeZone`; the raw value when it is not a date. */
|
|
62
|
+
export declare function formatDateTimeInZone(value: string | null | undefined, timeZone: string): string;
|
|
63
|
+
/** "Wed, Oct 1, 2026" for a YYYY-MM-DD calendar day. */
|
|
64
|
+
export declare function formatCalendarDay(day: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* The scheduled range in plain words. Whole days read as "Oct 1, 2026 to
|
|
67
|
+
* Oct 31, 2026" (the API's `to` is exclusive, so a midnight `to` ends the
|
|
68
|
+
* day before); anything else reads as "from X up to Y".
|
|
69
|
+
*/
|
|
70
|
+
export declare function formatPostRange(range: PostListRange): string;
|
|
71
|
+
/**
|
|
72
|
+
* One page of list_posts. Always says whether every matching post is shown,
|
|
73
|
+
* and when it is not, exactly which offset to call next, so an agent never
|
|
74
|
+
* mistakes the first page for the whole schedule.
|
|
75
|
+
*/
|
|
76
|
+
export declare function formatPostListPage(data: PostListPage, options: {
|
|
77
|
+
timeZone: string;
|
|
78
|
+
dashboardUrl?: string;
|
|
79
|
+
}): string;
|
|
80
|
+
/** The counts view of list_posts: exact totals per day, status, account, and brand. */
|
|
81
|
+
export declare function formatPostCounts(data: PostCountsResponse): string;
|
package/dist/lib/format.js
CHANGED
|
@@ -229,3 +229,161 @@ function isInCurrentMonth(value) {
|
|
|
229
229
|
const now = new Date();
|
|
230
230
|
return date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth();
|
|
231
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* The completeness line every paged list prints, so an agent always knows
|
|
234
|
+
* whether it has seen everything. Shared by the npm package and the hosted
|
|
235
|
+
* endpoint.
|
|
236
|
+
*/
|
|
237
|
+
export function formatPagingNote(input) {
|
|
238
|
+
const offset = input.offset ?? 0;
|
|
239
|
+
const first = offset + 1;
|
|
240
|
+
const last = offset + input.count;
|
|
241
|
+
const ofTotal = typeof input.total === 'number' ? ` of ${input.total}` : '';
|
|
242
|
+
if (input.hasMore) {
|
|
243
|
+
const next = typeof input.nextOffset === 'number' ? input.nextOffset : last;
|
|
244
|
+
return `**Not complete: showing ${first}-${last}${ofTotal} ${input.noun}.** More available: call ${input.tool} again with the same filters and offset=${next}. Keep paging until this says complete before drawing any conclusion about totals.`;
|
|
245
|
+
}
|
|
246
|
+
if (offset === 0) {
|
|
247
|
+
return `**Complete: all ${typeof input.total === 'number' ? input.total : input.count} ${input.noun} are shown.**`;
|
|
248
|
+
}
|
|
249
|
+
return `**Complete: this is the last page (${input.noun} ${first}-${last}${ofTotal}).**`;
|
|
250
|
+
}
|
|
251
|
+
function zonedParts(value, timeZone) {
|
|
252
|
+
const date = typeof value === 'string' ? new Date(value) : value;
|
|
253
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
254
|
+
timeZone,
|
|
255
|
+
weekday: 'short',
|
|
256
|
+
month: 'short',
|
|
257
|
+
day: 'numeric',
|
|
258
|
+
year: 'numeric',
|
|
259
|
+
hour: '2-digit',
|
|
260
|
+
minute: '2-digit',
|
|
261
|
+
second: '2-digit',
|
|
262
|
+
hourCycle: 'h23',
|
|
263
|
+
}).formatToParts(date);
|
|
264
|
+
return Object.fromEntries(parts.map((part) => [part.type, part.value]));
|
|
265
|
+
}
|
|
266
|
+
/** "Wed, Oct 1, 2026, 09:00" in `timeZone`; the raw value when it is not a date. */
|
|
267
|
+
export function formatDateTimeInZone(value, timeZone) {
|
|
268
|
+
if (!value)
|
|
269
|
+
return 'not scheduled';
|
|
270
|
+
const date = new Date(value);
|
|
271
|
+
if (Number.isNaN(date.getTime()))
|
|
272
|
+
return value;
|
|
273
|
+
const p = zonedParts(date, timeZone);
|
|
274
|
+
return `${p.weekday}, ${p.month} ${p.day}, ${p.year}, ${p.hour}:${p.minute}`;
|
|
275
|
+
}
|
|
276
|
+
/** "Wed, Oct 1, 2026" for a YYYY-MM-DD calendar day. */
|
|
277
|
+
export function formatCalendarDay(day) {
|
|
278
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(day);
|
|
279
|
+
if (!match)
|
|
280
|
+
return day;
|
|
281
|
+
const p = zonedParts(new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]), 12)), 'UTC');
|
|
282
|
+
return `${p.weekday}, ${p.month} ${p.day}, ${p.year}`;
|
|
283
|
+
}
|
|
284
|
+
function isZonedMidnight(value, timeZone) {
|
|
285
|
+
const p = zonedParts(value, timeZone);
|
|
286
|
+
return p.hour === '00' && p.minute === '00' && p.second === '00';
|
|
287
|
+
}
|
|
288
|
+
function zonedDay(value, timeZone) {
|
|
289
|
+
const p = zonedParts(value, timeZone);
|
|
290
|
+
return `${p.month} ${p.day}, ${p.year}`;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* The scheduled range in plain words. Whole days read as "Oct 1, 2026 to
|
|
294
|
+
* Oct 31, 2026" (the API's `to` is exclusive, so a midnight `to` ends the
|
|
295
|
+
* day before); anything else reads as "from X up to Y".
|
|
296
|
+
*/
|
|
297
|
+
export function formatPostRange(range) {
|
|
298
|
+
const tz = range.timezone;
|
|
299
|
+
if (range.from && range.to && isZonedMidnight(range.from, tz) && isZonedMidnight(range.to, tz)) {
|
|
300
|
+
const lastDay = new Date(Date.parse(range.to) - 1);
|
|
301
|
+
return `${zonedDay(range.from, tz)} to ${zonedDay(lastDay, tz)} (${tz})`;
|
|
302
|
+
}
|
|
303
|
+
const from = range.from ? `from ${formatDateTimeInZone(range.from, tz)}` : '';
|
|
304
|
+
const to = range.to ? `up to ${formatDateTimeInZone(range.to, tz)}` : '';
|
|
305
|
+
return `${[from, to].filter(Boolean).join(' ')} (${tz})`;
|
|
306
|
+
}
|
|
307
|
+
function approvalCell(post) {
|
|
308
|
+
if (!post.approval || post.approval.status === 'none')
|
|
309
|
+
return 'none';
|
|
310
|
+
return post.approval.waiting_on ? `${post.approval.status} (waiting on ${post.approval.waiting_on})` : post.approval.status;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* One page of list_posts. Always says whether every matching post is shown,
|
|
314
|
+
* and when it is not, exactly which offset to call next, so an agent never
|
|
315
|
+
* mistakes the first page for the whole schedule.
|
|
316
|
+
*/
|
|
317
|
+
export function formatPostListPage(data, options) {
|
|
318
|
+
const posts = Array.isArray(data.posts) ? data.posts : [];
|
|
319
|
+
const total = typeof data.total === 'number' ? data.total : posts.length;
|
|
320
|
+
const offset = typeof data.offset === 'number' ? data.offset : 0;
|
|
321
|
+
const tz = options.timeZone;
|
|
322
|
+
const rangeLine = data.range ? `Scheduled ${formatPostRange(data.range)}` : '';
|
|
323
|
+
const orderLine = data.order === 'asc' ? 'oldest first' : 'newest first';
|
|
324
|
+
if (posts.length === 0) {
|
|
325
|
+
const detail = total > 0
|
|
326
|
+
? `No posts on this page: offset ${offset} is past the last of the ${total} matching posts. Call again with a lower offset.`
|
|
327
|
+
: `No posts match these filters${data.range ? ` (${formatPostRange(data.range)})` : ''}. This is complete: there is nothing more to page through.`;
|
|
328
|
+
return [
|
|
329
|
+
mdTitle('No posts'),
|
|
330
|
+
detail,
|
|
331
|
+
'\n**Next step for the AI agent:** tell the user nothing matches that filter, then ask what they want to schedule next.',
|
|
332
|
+
].join('\n');
|
|
333
|
+
}
|
|
334
|
+
const first = offset + 1;
|
|
335
|
+
const last = offset + posts.length;
|
|
336
|
+
const hasMore = typeof data.has_more === 'boolean' ? data.has_more : last < total;
|
|
337
|
+
const nextOffset = typeof data.next_offset === 'number' ? data.next_offset : last;
|
|
338
|
+
const completeness = hasMore
|
|
339
|
+
? `**Not complete: showing ${first}-${last} of ${total} matching posts.** More available: call list_posts again with the same filters and offset=${nextOffset}. Keep paging until every post is shown before drawing any conclusion about totals, gaps, or how busy a day or client is. For "how many" questions, use view=counts.`
|
|
340
|
+
: first === 1
|
|
341
|
+
? `**Complete: all ${total} matching posts are shown.**`
|
|
342
|
+
: `**Complete: this is the last page (posts ${first}-${last} of ${total}).**`;
|
|
343
|
+
const table = mdTable(['Status', 'Post ID', `Scheduled (${tz})`, 'Type', 'Account ID', 'Brand ID', 'Approval', 'Caption'],
|
|
344
|
+
// The API post row carries account and brand IDs only (no joined names);
|
|
345
|
+
// list_accounts and list_brands, or view=counts, resolve them.
|
|
346
|
+
posts.map((post) => [
|
|
347
|
+
statusLabel(post.status),
|
|
348
|
+
code(String(post.id)),
|
|
349
|
+
formatDateTimeInZone(post.scheduled_at, tz),
|
|
350
|
+
post.post_type || 'post',
|
|
351
|
+
post.social_account_id != null ? String(post.social_account_id) : 'n/a',
|
|
352
|
+
post.brand_id || 'none',
|
|
353
|
+
approvalCell(post),
|
|
354
|
+
compactText(post.content, 80) || '(empty)',
|
|
355
|
+
]));
|
|
356
|
+
return [
|
|
357
|
+
mdTitle(hasMore || first > 1 ? `Posts ${first}-${last} of ${total}` : `Posts (${total})`, [rangeLine, orderLine, `times in ${tz}`].filter(Boolean).join(' · ')),
|
|
358
|
+
table,
|
|
359
|
+
completeness,
|
|
360
|
+
options.dashboardUrl ? `View in posterly: ${options.dashboardUrl}` : '',
|
|
361
|
+
].filter(Boolean).join('\n\n');
|
|
362
|
+
}
|
|
363
|
+
/** The counts view of list_posts: exact totals per day, status, account, and brand. */
|
|
364
|
+
export function formatPostCounts(data) {
|
|
365
|
+
const range = data.range;
|
|
366
|
+
const days = Array.isArray(data.by_day) ? data.by_day : [];
|
|
367
|
+
const emptyDays = days.filter((day) => day.count === 0).map((day) => formatCalendarDay(day.date));
|
|
368
|
+
const sections = [
|
|
369
|
+
mdTitle(`Post counts: ${data.total} ${data.total === 1 ? 'post' : 'posts'}`, range ? formatPostRange(range) : undefined),
|
|
370
|
+
data.truncated
|
|
371
|
+
? `**Partial grouping:** ${data.total} posts match, but only the first ${data.counted} were grouped below. The total is exact; narrow the range or filters for exact per-day numbers.`
|
|
372
|
+
: `**Complete:** every one of the ${data.total} matching posts is counted below.`,
|
|
373
|
+
mdSection('By day', mdTable(['Day', 'Posts'], days.map((day) => [formatCalendarDay(day.date), day.count]))),
|
|
374
|
+
// A truncated grouping only covers the earliest posts, so a day with a
|
|
375
|
+
// zero here may still have posts: never list "empty" days then.
|
|
376
|
+
days.length > 0 && !data.truncated ? `Days with no posts: ${emptyDays.length > 0 ? emptyDays.join('; ') : 'none'}.` : '',
|
|
377
|
+
data.truncated ? '_Per-day numbers above cover only the grouped posts, so a day showing 0 may still have posts. Narrow the range to check for empty days._' : '',
|
|
378
|
+
mdSection('By status', mdTable(['Status', 'Posts'], (data.by_status || []).map((row) => [statusLabel(row.status), row.count]))),
|
|
379
|
+
mdSection('By account', mdTable(['Account', 'Platform', 'Account ID', 'Posts'], (data.by_account || []).map((row) => [
|
|
380
|
+
row.username ? `@${row.username}` : row.account_id ? 'unknown account' : 'no account',
|
|
381
|
+
row.platform || 'n/a',
|
|
382
|
+
row.account_id ?? 'none',
|
|
383
|
+
row.count,
|
|
384
|
+
]))),
|
|
385
|
+
mdSection('By brand', mdTable(['Brand', 'Brand ID', 'Posts'], (data.by_brand || []).map((row) => [row.name || (row.brand_id ? 'unknown brand' : 'no brand'), row.brand_id ?? 'none', row.count]))),
|
|
386
|
+
'**Next step for the AI agent:** these totals are exact for the filters above. To read the posts themselves, call list_posts with view=list and the same filters, paging with offset until every post is shown.',
|
|
387
|
+
];
|
|
388
|
+
return sections.filter(Boolean).join('\n\n');
|
|
389
|
+
}
|
package/dist/lib/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const POSTERLY_MCP_VERSION = "0.
|
|
1
|
+
export declare const POSTERLY_MCP_VERSION = "0.49.0";
|
package/dist/lib/version.js
CHANGED
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
// tool set (minus the intentional pre-auth signup tools that only this stdio
|
|
6
6
|
// package exposes). `npm run check:mcp-parity` enforces both the version match
|
|
7
7
|
// and the tool-list match, and runs in the pre-commit hook.
|
|
8
|
-
export const POSTERLY_MCP_VERSION = '0.
|
|
8
|
+
export const POSTERLY_MCP_VERSION = '0.49.0';
|
|
@@ -12,12 +12,12 @@ export declare const auditGoogleBusinessProfileTool: {
|
|
|
12
12
|
account_id: z.ZodOptional<z.ZodNumber>;
|
|
13
13
|
location_id: z.ZodOptional<z.ZodString>;
|
|
14
14
|
}, "strip", z.ZodTypeAny, {
|
|
15
|
-
workspace_id?: string | undefined;
|
|
16
15
|
account_id?: number | undefined;
|
|
16
|
+
workspace_id?: string | undefined;
|
|
17
17
|
location_id?: string | undefined;
|
|
18
18
|
}, {
|
|
19
|
-
workspace_id?: string | undefined;
|
|
20
19
|
account_id?: number | undefined;
|
|
20
|
+
workspace_id?: string | undefined;
|
|
21
21
|
location_id?: string | undefined;
|
|
22
22
|
}>;
|
|
23
23
|
execute(client: PosterlyClient, input: {
|
|
@@ -21,8 +21,8 @@ export declare const connectAccountTool: {
|
|
|
21
21
|
connect_session_id: z.ZodOptional<z.ZodString>;
|
|
22
22
|
}, "strip", z.ZodTypeAny, {
|
|
23
23
|
platform: "telegram" | "bluesky" | "discord" | "wordpress" | "devto" | "hashnode" | "lemmy";
|
|
24
|
-
username?: string | undefined;
|
|
25
24
|
workspace_id?: string | undefined;
|
|
25
|
+
username?: string | undefined;
|
|
26
26
|
connect_session_id?: string | undefined;
|
|
27
27
|
bot_token?: string | undefined;
|
|
28
28
|
chat_id?: string | undefined;
|
|
@@ -36,8 +36,8 @@ export declare const connectAccountTool: {
|
|
|
36
36
|
password?: string | undefined;
|
|
37
37
|
}, {
|
|
38
38
|
platform: "telegram" | "bluesky" | "discord" | "wordpress" | "devto" | "hashnode" | "lemmy";
|
|
39
|
-
username?: string | undefined;
|
|
40
39
|
workspace_id?: string | undefined;
|
|
40
|
+
username?: string | undefined;
|
|
41
41
|
connect_session_id?: string | undefined;
|
|
42
42
|
bot_token?: string | undefined;
|
|
43
43
|
chat_id?: string | undefined;
|
|
@@ -1293,9 +1293,9 @@ export declare const createPostPayloadInputSchema: z.ZodObject<{
|
|
|
1293
1293
|
workspace_id: z.ZodOptional<z.ZodString>;
|
|
1294
1294
|
}, "strip", z.ZodTypeAny, {
|
|
1295
1295
|
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;
|
|
1296
|
-
username?: string | undefined;
|
|
1297
|
-
workspace_id?: string | undefined;
|
|
1298
1296
|
account_id?: string | undefined;
|
|
1297
|
+
workspace_id?: string | undefined;
|
|
1298
|
+
username?: string | undefined;
|
|
1299
1299
|
caption?: string | undefined;
|
|
1300
1300
|
scheduled_at?: string | undefined;
|
|
1301
1301
|
media_url?: string | undefined;
|
|
@@ -1494,9 +1494,9 @@ export declare const createPostPayloadInputSchema: z.ZodObject<{
|
|
|
1494
1494
|
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
1495
1495
|
}, {
|
|
1496
1496
|
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;
|
|
1497
|
-
username?: string | undefined;
|
|
1498
|
-
workspace_id?: string | undefined;
|
|
1499
1497
|
account_id?: string | undefined;
|
|
1498
|
+
workspace_id?: string | undefined;
|
|
1499
|
+
username?: string | undefined;
|
|
1500
1500
|
caption?: string | undefined;
|
|
1501
1501
|
scheduled_at?: string | undefined;
|
|
1502
1502
|
media_url?: string | undefined;
|
|
@@ -2350,9 +2350,9 @@ export declare const createPostInputSchema: z.ZodObject<{
|
|
|
2350
2350
|
}, "strip", z.ZodTypeAny, {
|
|
2351
2351
|
confirm: true;
|
|
2352
2352
|
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;
|
|
2353
|
-
username?: string | undefined;
|
|
2354
|
-
workspace_id?: string | undefined;
|
|
2355
2353
|
account_id?: string | undefined;
|
|
2354
|
+
workspace_id?: string | undefined;
|
|
2355
|
+
username?: string | undefined;
|
|
2356
2356
|
caption?: string | undefined;
|
|
2357
2357
|
scheduled_at?: string | undefined;
|
|
2358
2358
|
media_url?: string | undefined;
|
|
@@ -2552,9 +2552,9 @@ export declare const createPostInputSchema: z.ZodObject<{
|
|
|
2552
2552
|
}, {
|
|
2553
2553
|
confirm: true;
|
|
2554
2554
|
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;
|
|
2555
|
-
username?: string | undefined;
|
|
2556
|
-
workspace_id?: string | undefined;
|
|
2557
2555
|
account_id?: string | undefined;
|
|
2556
|
+
workspace_id?: string | undefined;
|
|
2557
|
+
username?: string | undefined;
|
|
2558
2558
|
caption?: string | undefined;
|
|
2559
2559
|
scheduled_at?: string | undefined;
|
|
2560
2560
|
media_url?: string | undefined;
|
|
@@ -3437,9 +3437,9 @@ export declare const createPostTool: {
|
|
|
3437
3437
|
}, "strip", z.ZodTypeAny, {
|
|
3438
3438
|
confirm: true;
|
|
3439
3439
|
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;
|
|
3440
|
-
username?: string | undefined;
|
|
3441
|
-
workspace_id?: string | undefined;
|
|
3442
3440
|
account_id?: string | undefined;
|
|
3441
|
+
workspace_id?: string | undefined;
|
|
3442
|
+
username?: string | undefined;
|
|
3443
3443
|
caption?: string | undefined;
|
|
3444
3444
|
scheduled_at?: string | undefined;
|
|
3445
3445
|
media_url?: string | undefined;
|
|
@@ -3639,9 +3639,9 @@ export declare const createPostTool: {
|
|
|
3639
3639
|
}, {
|
|
3640
3640
|
confirm: true;
|
|
3641
3641
|
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;
|
|
3642
|
-
username?: string | undefined;
|
|
3643
|
-
workspace_id?: string | undefined;
|
|
3644
3642
|
account_id?: string | undefined;
|
|
3643
|
+
workspace_id?: string | undefined;
|
|
3644
|
+
username?: string | undefined;
|
|
3645
3645
|
caption?: string | undefined;
|
|
3646
3646
|
scheduled_at?: string | undefined;
|
|
3647
3647
|
media_url?: string | undefined;
|