posterly-mcp-server 0.35.0 → 0.36.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 +1 -1
- package/dist/lib/api-client.d.ts +1 -0
- package/dist/lib/api-client.js +2 -1
- package/dist/lib/version.d.ts +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/tools/create-post.d.ts +576 -0
- package/dist/tools/create-post.js +28 -2
- package/dist/tools/create-posts-batch.d.ts +224 -0
- package/dist/tools/create-signed-upload.js +2 -1
- package/dist/tools/generate-image.d.ts +2 -2
- package/dist/tools/generate-video.d.ts +2 -2
- package/dist/tools/generate-video.js +1 -1
- package/dist/tools/update-post.d.ts +256 -0
- package/dist/tools/update-post.js +26 -0
- package/dist/tools/validate-post.d.ts +320 -0
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/lib/api-client.ts +9 -2
- package/src/lib/version.ts +1 -1
- package/src/tools/create-post.ts +28 -2
- package/src/tools/create-signed-upload.ts +2 -1
- package/src/tools/generate-video.ts +1 -1
- package/src/tools/update-post.ts +26 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "posterly-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
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.
|
|
6
|
+
"version": "0.36.0",
|
|
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.
|
|
17
|
+
"version": "0.36.0",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
|
20
20
|
},
|
package/src/lib/api-client.ts
CHANGED
|
@@ -1641,7 +1641,13 @@ export class PosterlyClient {
|
|
|
1641
1641
|
filename: string,
|
|
1642
1642
|
contentType: string,
|
|
1643
1643
|
size: number,
|
|
1644
|
-
): Promise<{
|
|
1644
|
+
): Promise<{
|
|
1645
|
+
upload_url: string;
|
|
1646
|
+
token: string;
|
|
1647
|
+
path: string;
|
|
1648
|
+
public_url: string;
|
|
1649
|
+
headers?: Record<string, string>;
|
|
1650
|
+
}> {
|
|
1645
1651
|
return this.request('POST', '/media/signed-upload', {
|
|
1646
1652
|
filename,
|
|
1647
1653
|
content_type: contentType,
|
|
@@ -1674,7 +1680,7 @@ export class PosterlyClient {
|
|
|
1674
1680
|
const SIZE_THRESHOLD = 3 * 1024 * 1024; // 3MB
|
|
1675
1681
|
|
|
1676
1682
|
if (fileBuffer.length > SIZE_THRESHOLD) {
|
|
1677
|
-
// Large file:
|
|
1683
|
+
// Large file: PUT to the object-storage signed URL (not a Vercel relay).
|
|
1678
1684
|
const signed = await this.getSignedUploadUrl(
|
|
1679
1685
|
input.filename,
|
|
1680
1686
|
contentType,
|
|
@@ -1685,6 +1691,7 @@ export class PosterlyClient {
|
|
|
1685
1691
|
method: 'PUT',
|
|
1686
1692
|
headers: {
|
|
1687
1693
|
'Content-Type': contentType,
|
|
1694
|
+
...(signed.headers || {}),
|
|
1688
1695
|
},
|
|
1689
1696
|
body: new Uint8Array(fileBuffer),
|
|
1690
1697
|
});
|
package/src/lib/version.ts
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.36.0';
|
package/src/tools/create-post.ts
CHANGED
|
@@ -21,6 +21,8 @@ export const instagramSettingsSchema = z.object({
|
|
|
21
21
|
media_alt_texts: z.record(z.string(), z.string()).optional(),
|
|
22
22
|
reel_cover_url: z.string().optional(),
|
|
23
23
|
reel_thumb_offset: z.number().nonnegative().optional(),
|
|
24
|
+
is_ai_generated: z.boolean().optional().describe('Label the parent Instagram media container as AI-generated. For carousels, set on the parent only.'),
|
|
25
|
+
audio_id: z.string().optional().describe('Licensed Instagram audio ID for Reels. Meta-linked Facebook Login accounts only.'),
|
|
24
26
|
});
|
|
25
27
|
|
|
26
28
|
export const platformSettingsSchema = z.object({
|
|
@@ -44,6 +46,15 @@ export const platformSettingsSchema = z.object({
|
|
|
44
46
|
topic_tag: z.string().optional(), // Threads single discovery topic tag
|
|
45
47
|
share_to_instagram: z.boolean().optional(), // Threads: also share the published post to the linked Instagram account as a Story
|
|
46
48
|
text_attachment: z.record(z.string(), z.unknown()).optional(),
|
|
49
|
+
is_ghost_post: z.boolean().optional(),
|
|
50
|
+
is_spoiler: z.boolean().optional(),
|
|
51
|
+
is_spoiler_media: z.boolean().optional(),
|
|
52
|
+
enable_reply_approvals: z.boolean().optional(),
|
|
53
|
+
text_entities: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
54
|
+
gif_attachment: z.object({
|
|
55
|
+
gif_id: z.string(),
|
|
56
|
+
provider: z.enum(['GIPHY']).optional(),
|
|
57
|
+
}).optional(),
|
|
47
58
|
// Facebook
|
|
48
59
|
text_format_preset_id: z.string().optional(),
|
|
49
60
|
// LinkedIn
|
|
@@ -52,6 +63,8 @@ export const platformSettingsSchema = z.object({
|
|
|
52
63
|
mentions: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
53
64
|
thumbnail_url: z.string().optional(),
|
|
54
65
|
thumbnail_method: z.string().optional(),
|
|
66
|
+
content_call_to_action_label: z.enum(['APPLY', 'DOWNLOAD', 'VIEW_QUOTE', 'LEARN_MORE', 'SIGN_UP', 'SUBSCRIBE', 'REGISTER', 'JOIN', 'ATTEND', 'REQUEST_DEMO', 'SEE_MORE', 'BUY_NOW', 'SHOP_NOW']).optional(),
|
|
67
|
+
content_landing_page: z.string().optional(),
|
|
55
68
|
// TikTok
|
|
56
69
|
privacy_level: z.string().optional(),
|
|
57
70
|
allow_comment: z.boolean().optional(),
|
|
@@ -75,20 +88,33 @@ export const platformSettingsSchema = z.object({
|
|
|
75
88
|
cover_image_url: z.string().optional(),
|
|
76
89
|
video_cover_url: z.string().optional(),
|
|
77
90
|
cover_image_method: z.enum(['upload', 'frame', 'api']).optional(),
|
|
91
|
+
product_tags: z.array(z.union([z.string(), z.record(z.string(), z.unknown())])).optional(),
|
|
78
92
|
// Google Business
|
|
79
93
|
event: z.record(z.string(), z.unknown()).optional(),
|
|
80
94
|
offer: z.record(z.string(), z.unknown()).optional(),
|
|
81
95
|
cta: z.record(z.string(), z.unknown()).optional(),
|
|
96
|
+
recurrence: z.record(z.string(), z.unknown()).optional(),
|
|
82
97
|
// YouTube
|
|
83
98
|
notify_subscribers: z.boolean().optional(),
|
|
84
99
|
add_shorts_hashtag: z.boolean().optional(),
|
|
85
|
-
|
|
100
|
+
brand_partner: z.string().optional(),
|
|
101
|
+
// Telegram
|
|
86
102
|
buttons: z.array(z.object({ text: z.string(), url: z.string() })).optional(),
|
|
103
|
+
start_timestamp: z.number().nonnegative().optional(),
|
|
104
|
+
is_live_photo: z.boolean().optional(),
|
|
87
105
|
// Bluesky
|
|
88
106
|
langs: z.array(z.string()).optional(),
|
|
89
107
|
content_label: z.enum(['sexual', 'nudity', 'porn', 'graphic-media']).optional(), // Bluesky content warning
|
|
90
108
|
quote_url: z.string().optional(), // Bluesky post URL/AT-URI to quote
|
|
91
109
|
disable_quotes: z.boolean().optional(), // Bluesky: prevent quote-posts of this post
|
|
110
|
+
quoted_status_id: z.string().optional(), // Mastodon quote
|
|
111
|
+
visibility: z.enum(['public', 'unlisted', 'private']).optional(),
|
|
112
|
+
spoiler_text: z.string().optional(),
|
|
113
|
+
markdown: z.boolean().optional(), // Slack Block Kit markdown
|
|
114
|
+
blocks: z.array(z.record(z.string(), z.unknown())).optional(), // Slack Block Kit
|
|
115
|
+
paid_partnership: z.boolean().optional(), // X paid partnership
|
|
116
|
+
is_ai_generated: z.boolean().optional(),
|
|
117
|
+
audio_id: z.string().optional(),
|
|
92
118
|
// tags (z.array of strings) also serve as Bluesky hidden discovery tags
|
|
93
119
|
// AI-content disclosure (TikTok is_aigc, X made_with_ai, YouTube containsSyntheticMedia, Pinterest ai_disclosures)
|
|
94
120
|
ai_generated: z.boolean().optional(),
|
|
@@ -204,7 +230,7 @@ export const createPostTool = {
|
|
|
204
230
|
'Provide either account_id OR username+platform to identify the account. If scheduled_at is omitted, the post publishes immediately. If workspace_id is omitted, the server resolves one from the social account, falling back to the caller\'s default (personal) workspace - pass workspace_id explicitly if the user has more than one workspace.\n\n' +
|
|
205
231
|
'MEDIA URLS: Posterly storage URLs are used as-is. Third-party HTTP(S) media URLs are copied into Posterly storage before the post is saved, so API-created posts do not later fail on expired signed URLs. Short-lived signed URLs must still be live when this tool runs. You can also call `upload_media_from_url` first if you want to upload once and reuse the returned Posterly URL across posts.\n\n' +
|
|
206
232
|
'THREADS: Pass `thread_posts` (an array of 2+ strings) to schedule a multi-post thread on X (Twitter) or Threads (Meta). The first entry is the lead post; the rest are published as replies in the same chain. X entries are capped at 280 characters each (4000 for verified, 25000 for organization accounts); Threads entries are capped at 500 characters each. When `thread_posts` is set, `caption` is ignored.\n\n' +
|
|
207
|
-
'PLATFORM SETTINGS: Pass `platform_settings` for composer-equivalent controls: Facebook story/reel/backgrounds, YouTube title/privacy/thumbnail/
|
|
233
|
+
'PLATFORM SETTINGS: Pass `platform_settings` for composer-equivalent controls: Facebook story/reel/backgrounds, YouTube title/privacy/thumbnail/brandPartner, LinkedIn document titles/mentions/polls/content_call_to_action_label (including BUY_NOW and SHOP_NOW), TikTok direct-post privacy/toggles/commercial disclosure, Pinterest board/title/link/product_tags, GBP event/offer/CTA/recurrence, X reply settings/polls/paid_partnership, Threads reply controls/text attachments/ghost posts/spoilers/reply approvals/GIPHY, Telegram polls/buttons/video cover/live photo, Slack markdown/blocks, Mastodon visibility/quote/poll (media+poll allowed), and Instagram feed/story/reel/carousel/native media tags/collaborators/first comments/trial Reels/is_ai_generated/audio_id. For Instagram, @username in caption is only a caption mention; `user_tags` creates native media tags and `collaborators` sends co-author invitations only when the selected Meta-linked account reports those capabilities. Call `get_platform_schema` with the account ID first. `instagram_settings` remains as a backwards-compatible alias.\n\n' +
|
|
208
234
|
'TIKTOK MEDIA: TikTok supports a single video or a photo slideshow of 1 to 35 images, never multiple videos. Image media auto-detects as a slideshow, so you usually do not set post_type or media_type for photos and every image is posted. Pass `platform_settings.media_type: "PHOTO"` to force a slideshow explicitly.',
|
|
209
235
|
inputSchema: createPostInputSchema,
|
|
210
236
|
|
|
@@ -4,7 +4,7 @@ import type { PosterlyClient } from '../lib/api-client.js';
|
|
|
4
4
|
export const createSignedUploadTool = {
|
|
5
5
|
name: 'create_signed_upload',
|
|
6
6
|
description:
|
|
7
|
-
'Create a signed upload URL for a larger image or video.
|
|
7
|
+
'Create a signed upload URL for a larger image or video. PUT the raw bytes to the returned upload_url (object storage) with the returned headers, then public_url can be used with create_post.',
|
|
8
8
|
inputSchema: z.object({
|
|
9
9
|
filename: z.string().describe('Filename with extension, e.g. launch-video.mp4'),
|
|
10
10
|
content_type: z.string().describe('MIME type, e.g. video/mp4'),
|
|
@@ -22,6 +22,7 @@ export const createSignedUploadTool = {
|
|
|
22
22
|
`• Token: ${signed.token}`,
|
|
23
23
|
`• Storage path: ${signed.path}`,
|
|
24
24
|
`• Public URL after upload: ${signed.public_url}`,
|
|
25
|
+
'PUT the raw file bytes to the upload URL with the same Content-Type. Then pass public_url to create_post.',
|
|
25
26
|
].join('\n');
|
|
26
27
|
},
|
|
27
28
|
};
|
|
@@ -7,7 +7,7 @@ export const generateVideoTool = {
|
|
|
7
7
|
'Queue an AI video generation job with Google Veo 3.1 or xAI Grok Imagine Video 1.5. COSTS CREDITS: confirm provider, prompt, duration, resolution, aspect ratio, audio choice, and credit cost with the user before calling. Grok supports 1–15 seconds and 480p–1080p; reference-to-video is capped at 720p. Poll get_video_job for status and final video_url.',
|
|
8
8
|
inputSchema: z.object({
|
|
9
9
|
provider: z.enum(['google', 'xai']).default('google').optional(),
|
|
10
|
-
prompt: z.string().min(5).max(
|
|
10
|
+
prompt: z.string().min(5).max(4000),
|
|
11
11
|
negative_prompt: z.string().max(500).optional(),
|
|
12
12
|
aspect_ratio: z.enum(['1:1', '16:9', '9:16', '4:3', '3:4', '3:2', '2:3']).default('16:9').optional(),
|
|
13
13
|
duration_seconds: z.number().int().min(1).max(15).default(8).optional(),
|
package/src/tools/update-post.ts
CHANGED
|
@@ -21,6 +21,8 @@ const instagramSettingsSchema = z.object({
|
|
|
21
21
|
reel_cover_url: z.string().optional(),
|
|
22
22
|
reel_cover_method: z.string().optional(),
|
|
23
23
|
reel_thumb_offset: z.number().nonnegative().optional(),
|
|
24
|
+
is_ai_generated: z.boolean().optional().describe('Label the parent Instagram media container as AI-generated. For carousels, set on the parent only.'),
|
|
25
|
+
audio_id: z.string().optional().describe('Licensed Instagram audio ID for Reels. Meta-linked Facebook Login accounts only.'),
|
|
24
26
|
});
|
|
25
27
|
|
|
26
28
|
const platformSettingsSchema = z.object({
|
|
@@ -37,12 +39,23 @@ const platformSettingsSchema = z.object({
|
|
|
37
39
|
topic_tag: z.string().optional(),
|
|
38
40
|
share_to_instagram: z.boolean().optional(), // Threads: also share the published post to the linked Instagram account as a Story
|
|
39
41
|
text_attachment: z.record(z.string(), z.unknown()).optional(),
|
|
42
|
+
is_ghost_post: z.boolean().optional(),
|
|
43
|
+
is_spoiler: z.boolean().optional(),
|
|
44
|
+
is_spoiler_media: z.boolean().optional(),
|
|
45
|
+
enable_reply_approvals: z.boolean().optional(),
|
|
46
|
+
text_entities: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
47
|
+
gif_attachment: z.object({
|
|
48
|
+
gif_id: z.string(),
|
|
49
|
+
provider: z.enum(['GIPHY']).optional(),
|
|
50
|
+
}).optional(),
|
|
40
51
|
text_format_preset_id: z.string().optional(),
|
|
41
52
|
document_title: z.string().optional(),
|
|
42
53
|
document_filename: z.string().optional(),
|
|
43
54
|
mentions: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
44
55
|
thumbnail_url: z.string().optional(),
|
|
45
56
|
thumbnail_method: z.string().optional(),
|
|
57
|
+
content_call_to_action_label: z.enum(['APPLY', 'DOWNLOAD', 'VIEW_QUOTE', 'LEARN_MORE', 'SIGN_UP', 'SUBSCRIBE', 'REGISTER', 'JOIN', 'ATTEND', 'REQUEST_DEMO', 'SEE_MORE', 'BUY_NOW', 'SHOP_NOW']).optional(),
|
|
58
|
+
content_landing_page: z.string().optional(),
|
|
46
59
|
privacy_level: z.string().optional(),
|
|
47
60
|
allow_comment: z.boolean().optional(),
|
|
48
61
|
allow_duet: z.boolean().optional(),
|
|
@@ -62,16 +75,29 @@ const platformSettingsSchema = z.object({
|
|
|
62
75
|
cover_image_url: z.string().optional(),
|
|
63
76
|
video_cover_url: z.string().optional(),
|
|
64
77
|
cover_image_method: z.enum(['upload', 'frame', 'api']).optional(),
|
|
78
|
+
product_tags: z.array(z.union([z.string(), z.record(z.string(), z.unknown())])).optional(),
|
|
65
79
|
event: z.record(z.string(), z.unknown()).optional(),
|
|
66
80
|
offer: z.record(z.string(), z.unknown()).optional(),
|
|
67
81
|
cta: z.record(z.string(), z.unknown()).optional(),
|
|
82
|
+
recurrence: z.record(z.string(), z.unknown()).optional(),
|
|
68
83
|
notify_subscribers: z.boolean().optional(),
|
|
69
84
|
add_shorts_hashtag: z.boolean().optional(),
|
|
85
|
+
brand_partner: z.string().optional(),
|
|
70
86
|
buttons: z.array(z.object({ text: z.string(), url: z.string() })).optional(),
|
|
87
|
+
start_timestamp: z.number().nonnegative().optional(),
|
|
88
|
+
is_live_photo: z.boolean().optional(),
|
|
71
89
|
langs: z.array(z.string()).optional(),
|
|
72
90
|
content_label: z.enum(['sexual', 'nudity', 'porn', 'graphic-media']).optional(),
|
|
73
91
|
quote_url: z.string().optional(),
|
|
74
92
|
disable_quotes: z.boolean().optional(),
|
|
93
|
+
quoted_status_id: z.string().optional(),
|
|
94
|
+
visibility: z.enum(['public', 'unlisted', 'private']).optional(),
|
|
95
|
+
spoiler_text: z.string().optional(),
|
|
96
|
+
markdown: z.boolean().optional(),
|
|
97
|
+
blocks: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
98
|
+
paid_partnership: z.boolean().optional(),
|
|
99
|
+
is_ai_generated: z.boolean().optional(),
|
|
100
|
+
audio_id: z.string().optional(),
|
|
75
101
|
ai_generated: z.boolean().optional(),
|
|
76
102
|
}).passthrough();
|
|
77
103
|
|