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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posterly-mcp-server",
3
- "version": "0.35.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.35.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.35.0",
17
+ "version": "0.36.0",
18
18
  "transport": {
19
19
  "type": "stdio"
20
20
  },
@@ -1641,7 +1641,13 @@ export class PosterlyClient {
1641
1641
  filename: string,
1642
1642
  contentType: string,
1643
1643
  size: number,
1644
- ): Promise<{ upload_url: string; token: string; path: string; public_url: string }> {
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: use signed URL so the API can validate actual bytes before storage.
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
  });
@@ -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.35.0';
8
+ export const POSTERLY_MCP_VERSION = '0.36.0';
@@ -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
- // Telegram inline CTA buttons
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/playlist, LinkedIn document titles/mentions/polls (poll.question, poll.options 2-4, poll.duration ONE_DAY|THREE_DAYS|SEVEN_DAYS|FOURTEEN_DAYS; text-only)/video thumbnails, TikTok direct-post privacy/toggles/commercial disclosure, Pinterest post_type (image/video/carousel), board (from pinterest.boards helper), title, link, and cover_image_url (video Pins), GBP event/offer/CTA, X reply settings/polls, Threads reply controls/text attachments, Bluesky languages/alt text, and Instagram feed/story/reel/carousel/native media tags/collaborators/first comments/trial Reels. 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' +
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. Upload the binary to upload_url; the API validates actual bytes before storage, then public_url can be used with create_post.',
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(1024),
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(),
@@ -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