posterly-mcp-server 0.48.4 → 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 +18 -3
- package/dist/index.js +4 -0
- package/dist/lib/api-client.d.ts +149 -12
- package/dist/lib/api-client.js +59 -16
- 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-brands.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/list-publishing-pauses.d.ts +19 -0
- package/dist/tools/list-publishing-pauses.js +81 -0
- 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 +35 -0
- package/dist/tools/manage-publishing-pause.js +47 -0
- 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/index.ts +4 -0
- package/src/lib/api-client.ts +172 -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/src/tools/list-publishing-pauses.ts +98 -0
- package/src/tools/manage-publishing-pause.ts +62 -0
- package/tool-annotations.json +5 -0
- package/tool-registry.json +173 -6
package/src/tools/get-updates.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
import { formatPagingNote } from '../lib/format.js';
|
|
3
4
|
|
|
4
5
|
export const getUpdatesTool = {
|
|
5
6
|
name: 'get_updates',
|
|
6
7
|
description:
|
|
7
|
-
'Get the latest posterly product updates and news from the updates feed: new features, improvements, and fixes. Requires an active posterly subscription.',
|
|
8
|
+
'Get the latest posterly product updates and news from the updates feed: new features, improvements, and fixes, newest first. The output says whether more updates exist; page with offset or narrow with since. Requires an active posterly subscription.',
|
|
8
9
|
inputSchema: z.object({
|
|
9
10
|
limit: z.number().int().min(1).max(50).optional().describe('Number of updates to return. Default 10, max 50.'),
|
|
11
|
+
offset: z.number().int().min(0).optional().describe('How many updates to skip. Use the offset the previous page gives to get the next page.'),
|
|
10
12
|
since: z.string().optional().describe('ISO date. Return updates published on or after this date.'),
|
|
11
13
|
include_content: z.boolean().optional().describe('Include the full markdown body of each update. Defaults to false.'),
|
|
12
14
|
}),
|
|
@@ -15,6 +17,7 @@ export const getUpdatesTool = {
|
|
|
15
17
|
client: PosterlyClient,
|
|
16
18
|
input: {
|
|
17
19
|
limit?: number;
|
|
20
|
+
offset?: number;
|
|
18
21
|
since?: string;
|
|
19
22
|
include_content?: boolean;
|
|
20
23
|
},
|
|
@@ -36,6 +39,17 @@ export const getUpdatesTool = {
|
|
|
36
39
|
}
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
lines.push('', formatPagingNote({
|
|
43
|
+
noun: 'updates',
|
|
44
|
+
tool: 'get_updates',
|
|
45
|
+
offset: result.offset ?? input.offset ?? 0,
|
|
46
|
+
count: result.updates.length,
|
|
47
|
+
total: result.total,
|
|
48
|
+
// An API older than has_more: a full page may mean more exist.
|
|
49
|
+
hasMore: result.has_more ?? result.updates.length >= (input.limit || 10),
|
|
50
|
+
nextOffset: result.next_offset,
|
|
51
|
+
}));
|
|
52
|
+
|
|
39
53
|
return lines.join('\n');
|
|
40
54
|
},
|
|
41
55
|
};
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
import { formatPagingNote } from '../lib/format.js';
|
|
3
4
|
|
|
4
5
|
export const listActivityTool = {
|
|
5
6
|
name: 'list_activity',
|
|
6
7
|
description:
|
|
7
|
-
'List recent activity and publish events for posts, including status changes, publish attempts, failures, and retries. Use this as the agent notifications feed.',
|
|
8
|
+
'List recent activity and publish events for posts, including status changes, publish attempts, failures, and retries, newest first. Use this as the agent notifications feed. The output says whether more events exist; page with offset (offset + limit up to 1000) or narrow with since or post_id before concluding nothing else happened.',
|
|
8
9
|
inputSchema: z.object({
|
|
9
10
|
workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
|
|
10
11
|
post_id: z.number().int().positive().optional().describe('Filter to one post ID.'),
|
|
11
12
|
since: z.string().optional().describe('ISO datetime. Return events created at or after this time.'),
|
|
12
13
|
type: z.enum(['all', 'activity', 'publish']).optional().describe('Event source filter. Defaults to all.'),
|
|
13
14
|
limit: z.number().int().min(1).max(100).optional().describe('Number of items to return. Default 50, max 100.'),
|
|
15
|
+
offset: z.number().int().min(0).optional().describe('How many events to skip. Use the offset the previous page gives to get the next page.'),
|
|
14
16
|
}),
|
|
15
17
|
|
|
16
18
|
async execute(
|
|
@@ -21,15 +23,18 @@ export const listActivityTool = {
|
|
|
21
23
|
since?: string;
|
|
22
24
|
type?: 'all' | 'activity' | 'publish';
|
|
23
25
|
limit?: number;
|
|
26
|
+
offset?: number;
|
|
24
27
|
},
|
|
25
28
|
) {
|
|
26
29
|
const result = await client.listActivity(input);
|
|
27
30
|
if (result.items.length === 0) {
|
|
28
|
-
return
|
|
31
|
+
return (input.offset ?? 0) > 0
|
|
32
|
+
? `No activity on this page: offset ${input.offset} is past the last matching event.`
|
|
33
|
+
: 'No activity found matching your filters.';
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
const lines = [
|
|
32
|
-
`Activity (${result.items.length}
|
|
37
|
+
`Activity (${result.items.length} on this page):`,
|
|
33
38
|
];
|
|
34
39
|
|
|
35
40
|
for (const item of result.items) {
|
|
@@ -45,6 +50,16 @@ export const listActivityTool = {
|
|
|
45
50
|
}
|
|
46
51
|
}
|
|
47
52
|
|
|
53
|
+
lines.push('', formatPagingNote({
|
|
54
|
+
noun: 'events',
|
|
55
|
+
tool: 'list_activity',
|
|
56
|
+
offset: result.offset ?? input.offset ?? 0,
|
|
57
|
+
count: result.items.length,
|
|
58
|
+
// An API older than has_more: a full page may mean more exist.
|
|
59
|
+
hasMore: result.has_more ?? result.items.length >= (result.limit || 50),
|
|
60
|
+
nextOffset: result.next_offset,
|
|
61
|
+
}));
|
|
62
|
+
|
|
48
63
|
return lines.join('\n');
|
|
49
64
|
},
|
|
50
65
|
};
|
package/src/tools/list-media.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
import { formatPagingNote } from '../lib/format.js';
|
|
3
4
|
|
|
4
5
|
export const listMediaTool = {
|
|
5
6
|
name: 'list_media',
|
|
6
7
|
description:
|
|
7
|
-
'List the newest media assets for the authenticated user. After create_media_drop, pass drop_session_id to see files the human uploaded on /drop/<token>. Use public_url with validate_post / create_post.',
|
|
8
|
+
'List the newest media assets for the authenticated user. After create_media_drop, pass drop_session_id to see files the human uploaded on /drop/<token>. Use public_url with validate_post / create_post. The output says whether every matching file is shown; page with offset (offset + limit up to 1000) when it is not.',
|
|
8
9
|
inputSchema: z.object({
|
|
9
10
|
limit: z
|
|
10
11
|
.number()
|
|
@@ -13,6 +14,12 @@ export const listMediaTool = {
|
|
|
13
14
|
.max(50)
|
|
14
15
|
.optional()
|
|
15
16
|
.describe('Number of assets to return (default 10, max 50)'),
|
|
17
|
+
offset: z
|
|
18
|
+
.number()
|
|
19
|
+
.int()
|
|
20
|
+
.min(0)
|
|
21
|
+
.optional()
|
|
22
|
+
.describe('How many assets to skip. Use the offset the previous page gives to get the next page.'),
|
|
16
23
|
drop_session_id: z
|
|
17
24
|
.string()
|
|
18
25
|
.optional()
|
|
@@ -21,14 +28,18 @@ export const listMediaTool = {
|
|
|
21
28
|
|
|
22
29
|
async execute(
|
|
23
30
|
client: PosterlyClient,
|
|
24
|
-
input: { limit?: number; drop_session_id?: string },
|
|
31
|
+
input: { limit?: number; offset?: number; drop_session_id?: string },
|
|
25
32
|
) {
|
|
26
33
|
const result = await client.listMedia({
|
|
27
34
|
limit: input.limit,
|
|
35
|
+
offset: input.offset,
|
|
28
36
|
dropSessionId: input.drop_session_id,
|
|
29
37
|
});
|
|
30
38
|
|
|
31
39
|
if (result.media.length === 0) {
|
|
40
|
+
if ((input.offset ?? 0) > 0 && result.total > 0) {
|
|
41
|
+
return `No media on this page: offset ${input.offset} is past the last of the ${result.total} matching files.`;
|
|
42
|
+
}
|
|
32
43
|
return 'No media found. If you just sent a drop URL, wait for the user to finish uploading, then call list_media again.';
|
|
33
44
|
}
|
|
34
45
|
|
|
@@ -38,10 +49,20 @@ export const listMediaTool = {
|
|
|
38
49
|
return `• ${item.filename} (${item.mime || 'unknown'}, ${size})${drop}\n public_url: ${item.public_url}`;
|
|
39
50
|
});
|
|
40
51
|
|
|
52
|
+
const offset = result.offset ?? input.offset ?? 0;
|
|
41
53
|
return [
|
|
42
54
|
`Media (${result.media.length} of ${result.total}):`,
|
|
43
55
|
...lines,
|
|
44
56
|
'',
|
|
57
|
+
formatPagingNote({
|
|
58
|
+
noun: 'files',
|
|
59
|
+
tool: 'list_media',
|
|
60
|
+
offset,
|
|
61
|
+
count: result.media.length,
|
|
62
|
+
total: result.total,
|
|
63
|
+
hasMore: result.has_more ?? offset + result.media.length < result.total,
|
|
64
|
+
nextOffset: result.next_offset,
|
|
65
|
+
}),
|
|
45
66
|
'Pass public_url to validate_post / create_post. Do not invent URLs.',
|
|
46
67
|
].join('\n');
|
|
47
68
|
},
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
-
import { mdTitle, mdSection, mdKeyValue, mdEmpty, truncateText } from '../lib/format.js';
|
|
3
|
+
import { formatPagingNote, mdTitle, mdSection, mdKeyValue, mdEmpty, truncateText } from '../lib/format.js';
|
|
4
4
|
|
|
5
5
|
export const listPostSuggestionsTool = {
|
|
6
6
|
name: 'list_post_suggestions',
|
|
7
7
|
description:
|
|
8
|
-
"List proactive post suggestions: evidence-based weekly drafts posterly writes in each account's learned voice, each with a rationale tying it back to what has performed well for that account. Filter by account_id and status (pending, scheduled, dismissed; default pending). Read-only; requires a Pro plan or higher. Use dismiss_suggestion to hide one.",
|
|
8
|
+
"List proactive post suggestions: evidence-based weekly drafts posterly writes in each account's learned voice, each with a rationale tying it back to what has performed well for that account. Filter by account_id and status (pending, scheduled, dismissed; default pending). The output says whether every matching suggestion is shown; page with offset until it does. Read-only; requires a Pro plan or higher. Use dismiss_suggestion to hide one.",
|
|
9
9
|
inputSchema: z.object({
|
|
10
10
|
account_id: z
|
|
11
11
|
.union([z.string(), z.number()])
|
|
@@ -16,6 +16,7 @@ export const listPostSuggestionsTool = {
|
|
|
16
16
|
.optional()
|
|
17
17
|
.describe('Suggestion status to list (default pending).'),
|
|
18
18
|
limit: z.number().int().min(1).max(100).optional().describe('Max suggestions to return (default 20).'),
|
|
19
|
+
offset: z.number().int().min(0).optional().describe('How many suggestions to skip. Use the offset the previous page gives to get the next page.'),
|
|
19
20
|
}),
|
|
20
21
|
|
|
21
22
|
async execute(
|
|
@@ -24,9 +25,11 @@ export const listPostSuggestionsTool = {
|
|
|
24
25
|
account_id?: string | number;
|
|
25
26
|
status?: 'pending' | 'scheduled' | 'dismissed';
|
|
26
27
|
limit?: number;
|
|
28
|
+
offset?: number;
|
|
27
29
|
},
|
|
28
30
|
) {
|
|
29
|
-
const
|
|
31
|
+
const page = await client.listPostSuggestions(input);
|
|
32
|
+
const { suggestions } = page;
|
|
30
33
|
|
|
31
34
|
if (!suggestions.length) {
|
|
32
35
|
return mdEmpty(
|
|
@@ -51,6 +54,16 @@ export const listPostSuggestionsTool = {
|
|
|
51
54
|
);
|
|
52
55
|
});
|
|
53
56
|
|
|
54
|
-
|
|
57
|
+
const note = formatPagingNote({
|
|
58
|
+
noun: 'suggestions',
|
|
59
|
+
tool: 'list_post_suggestions',
|
|
60
|
+
offset: page.offset ?? input.offset ?? 0,
|
|
61
|
+
count: suggestions.length,
|
|
62
|
+
total: page.total,
|
|
63
|
+
// An API older than has_more: a full page may mean more exist.
|
|
64
|
+
hasMore: page.has_more ?? suggestions.length >= (input.limit || 20),
|
|
65
|
+
nextOffset: page.next_offset,
|
|
66
|
+
});
|
|
67
|
+
return [mdTitle(`Post suggestions (${suggestions.length})`), ...blocks, note].join('\n\n');
|
|
55
68
|
},
|
|
56
69
|
};
|
package/src/tools/list-posts.ts
CHANGED
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
-
import { dashboardUrlForPostList,
|
|
2
|
+
import type { ListPostsParams, PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
import { dashboardUrlForPostList, formatPostCounts, formatPostListPage } from '../lib/format.js';
|
|
4
|
+
import { validateArgs, type ToolContext } from '../lib/tool-runtime.js';
|
|
4
5
|
import { SUPPORTED_PLATFORM_INPUTS, SUPPORTED_PLATFORM_IDS } from '../generated/platform-manifest.js';
|
|
5
6
|
|
|
7
|
+
type ListPostsInput = ListPostsParams & {
|
|
8
|
+
view?: 'list' | 'counts';
|
|
9
|
+
limit?: number;
|
|
10
|
+
offset?: number;
|
|
11
|
+
order?: 'asc' | 'desc';
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const LIST_POSTS_DESCRIPTION =
|
|
15
|
+
'Posts in your workspaces, teammates\' too, per your role there (brand-locked seats only see their assigned brands). If workspace_id is omitted, this searches every workspace you belong to. ' +
|
|
16
|
+
'view=list (default): posts filtered by status (scheduled, published, failed, draft), platform, account_id, workspace_id, brand_id, approval_status, approval_waiting_on, and a scheduled date range: scheduled_from (inclusive) and scheduled_to (a plain date includes that whole day), read in timezone (IANA, default UTC; also the zone times are shown in). Returns limit posts (default 20, max 200) starting at offset; with a date range the order defaults to oldest first (order=asc|desc). ' +
|
|
17
|
+
'The output always says whether every matching post was shown. If it says more are available, call again with the offset it gives and keep paging until all are shown before drawing any conclusion about totals, gaps, or how busy a client is. ' +
|
|
18
|
+
'view=counts (scheduled_from and scheduled_to required, up to 366 days): exact totals per day, status, account, and brand for the same filters (limit, offset, and order are ignored); use it for "how many" or "is this month full" questions. ' +
|
|
19
|
+
'approval_waiting_on=client is the same queue as approval_status=pending_client. Each post includes approval.pending_client_approval.';
|
|
20
|
+
|
|
6
21
|
export const listPostsTool = {
|
|
7
22
|
name: 'list_posts',
|
|
8
|
-
description:
|
|
9
|
-
'List upcoming or recent posts. Filter by status (scheduled, published, failed, draft), platform, account_id, workspace_id, approval_status, approval_waiting_on, or brand_id. approval_waiting_on=client is the same queue as approval_status=pending_client. Each post includes approval.pending_client_approval. Returns posts in your workspaces, teammates\' too, per your role there (brand-locked seats only see their assigned brands). If workspace_id is omitted, this searches every workspace you belong to.',
|
|
23
|
+
description: LIST_POSTS_DESCRIPTION,
|
|
10
24
|
inputSchema: z.object({
|
|
25
|
+
view: z
|
|
26
|
+
.enum(['list', 'counts'])
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('list (default): the posts themselves, one page at a time. counts: exact totals per day, status, account, and brand (needs scheduled_from and scheduled_to).'),
|
|
11
29
|
status: z
|
|
12
30
|
.string()
|
|
13
31
|
.optional()
|
|
@@ -20,12 +38,34 @@ export const listPostsTool = {
|
|
|
20
38
|
.string()
|
|
21
39
|
.optional()
|
|
22
40
|
.describe('Filter to a specific social account ID (from list_accounts).'),
|
|
41
|
+
scheduled_from: z
|
|
42
|
+
.string()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe('Start of the scheduled range, inclusive: a date (2026-10-01) or an ISO date-time. Plain dates and times without an offset are read in timezone.'),
|
|
45
|
+
scheduled_to: z
|
|
46
|
+
.string()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe('End of the scheduled range: a plain date (2026-10-31) includes that whole day; an ISO date-time is exclusive.'),
|
|
49
|
+
timezone: z
|
|
50
|
+
.string()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe('IANA time zone such as Europe/London for reading plain dates and showing times. Default UTC.'),
|
|
53
|
+
order: z
|
|
54
|
+
.enum(['asc', 'desc'])
|
|
55
|
+
.optional()
|
|
56
|
+
.describe('asc (oldest first) or desc (newest first). Default asc with a date range, otherwise desc. Ignored by view=counts.'),
|
|
23
57
|
limit: z
|
|
24
58
|
.number()
|
|
25
59
|
.min(1)
|
|
26
|
-
.max(
|
|
60
|
+
.max(200)
|
|
61
|
+
.optional()
|
|
62
|
+
.describe('Posts per page (default 20, max 200). Ignored by view=counts.'),
|
|
63
|
+
offset: z
|
|
64
|
+
.number()
|
|
65
|
+
.int()
|
|
66
|
+
.min(0)
|
|
27
67
|
.optional()
|
|
28
|
-
.describe('
|
|
68
|
+
.describe('How many matching posts to skip. Use the offset the previous page gives to get the next page. Ignored by view=counts.'),
|
|
29
69
|
workspace_id: z
|
|
30
70
|
.string()
|
|
31
71
|
.optional()
|
|
@@ -44,48 +84,37 @@ export const listPostsTool = {
|
|
|
44
84
|
.describe('Filter to posts assigned to a specific brand/client (the id from list_brands).'),
|
|
45
85
|
}),
|
|
46
86
|
|
|
47
|
-
async execute(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
platform?: string;
|
|
52
|
-
account_id?: string;
|
|
53
|
-
limit?: number;
|
|
54
|
-
workspace_id?: string;
|
|
55
|
-
approval_status?: string;
|
|
56
|
-
approval_waiting_on?: 'client' | 'team';
|
|
57
|
-
brand_id?: string;
|
|
58
|
-
}
|
|
59
|
-
) {
|
|
60
|
-
const { posts, total } = await client.listPosts({
|
|
61
|
-
...input,
|
|
62
|
-
limit: input.limit || 20,
|
|
63
|
-
});
|
|
87
|
+
async execute(client: PosterlyClient, input: ListPostsInput, ctx: ToolContext) {
|
|
88
|
+
const { mode, args } = validateArgs(ctx, 'list_posts', input);
|
|
89
|
+
const filters = postFilters(args as ListPostsInput);
|
|
90
|
+
const timeZone = filters.timezone || 'UTC';
|
|
64
91
|
|
|
65
|
-
if (
|
|
66
|
-
return
|
|
92
|
+
if (mode === 'counts') {
|
|
93
|
+
return formatPostCounts(await client.countPosts(filters));
|
|
67
94
|
}
|
|
68
95
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (p.approval.waiting_on) approvalBits.push(`waiting on: ${p.approval.waiting_on}`);
|
|
76
|
-
}
|
|
77
|
-
if (p.brand_id) approvalBits.push(`brand: ${p.brand_id}`);
|
|
78
|
-
if (p.content_plan_id) approvalBits.push(`plan: ${p.content_plan_id}`);
|
|
79
|
-
const approvalSuffix = approvalBits.length > 0 ? ` [${approvalBits.join(', ')}]` : '';
|
|
80
|
-
return `• [${p.status}] #${p.id}: ${caption} (${date})${approvalSuffix}`;
|
|
96
|
+
const hasRange = Boolean(filters.scheduled_from || filters.scheduled_to);
|
|
97
|
+
const page = await client.listPosts({
|
|
98
|
+
...filters,
|
|
99
|
+
limit: (args as ListPostsInput).limit || 20,
|
|
100
|
+
offset: (args as ListPostsInput).offset,
|
|
101
|
+
order: (args as ListPostsInput).order ?? (hasRange ? 'asc' : undefined),
|
|
81
102
|
});
|
|
82
|
-
|
|
83
|
-
return [
|
|
84
|
-
`Posts (${posts.length} of ${total}):`,
|
|
85
|
-
...lines,
|
|
86
|
-
'',
|
|
87
|
-
`View in posterly: ${dashboardUrlForPostList(posts)}`,
|
|
88
|
-
'Next step for the AI agent: summarize the queue in plain language and use the dashboard link above if the user wants to inspect it in posterly.',
|
|
89
|
-
].join('\n');
|
|
103
|
+
return formatPostListPage(page, { timeZone, dashboardUrl: dashboardUrlForPostList(page.posts || []) });
|
|
90
104
|
},
|
|
91
105
|
};
|
|
106
|
+
|
|
107
|
+
function postFilters(input: ListPostsInput): ListPostsParams {
|
|
108
|
+
return {
|
|
109
|
+
status: input.status,
|
|
110
|
+
platform: input.platform,
|
|
111
|
+
account_id: input.account_id,
|
|
112
|
+
workspace_id: input.workspace_id,
|
|
113
|
+
approval_status: input.approval_status,
|
|
114
|
+
approval_waiting_on: input.approval_waiting_on,
|
|
115
|
+
brand_id: input.brand_id,
|
|
116
|
+
scheduled_from: input.scheduled_from,
|
|
117
|
+
scheduled_to: input.scheduled_to,
|
|
118
|
+
timezone: input.timezone,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -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
|
+
};
|
package/tool-annotations.json
CHANGED
|
@@ -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",
|