create_posts
Create and schedule social media posts in batches of up to 15, targeting specific social accounts with platform-specific controls.
Instructions
Create and schedule social media posts. Supports batch creation (up to 15 posts). Each post targets a specific social account.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| posts | Yes | Array of posts to create | |
| status | No | Post status. SCHEDULED requires scheduledAt on all posts. | SCHEDULED |
| approvalStatus | No | Approval workflow status | APPROVED |
| controls | Yes | Platform-specific controls (shared across all posts in the batch) |
Implementation Reference
- src/tools/posts.ts:187-199 (handler)The handler function for the create_posts tool. Sends a POST request to '/social-posts' with the posts array, status, approvalStatus, and controls, then returns the response (postIds array) as JSON text content.
async (input) => { const data = await client.post<{ postIds: string[] }>('/social-posts', { posts: input.posts, status: input.status, approvalStatus: input.approvalStatus, controls: input.controls, }); return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }], }; }, ); - src/tools/posts.ts:85-186 (schema)Input schema for create_posts. Defines the posts array (up to 15 posts, each with content, firstComment, mediaItems, scheduledAt, socialMediaId), status (DRAFT or SCHEDULED), approvalStatus, and platform-specific controls (X, TikTok, Instagram, YouTube, Facebook, GBP, Pinterest, LinkedIn). The 'jsonParse' helper preprocesses stringified JSON for MCP clients that stringify complex params.
{ posts: jsonParse( z .array( z.object({ content: z.string().describe('Post text content'), firstComment: z.string().optional().describe('First comment to add after publishing'), mediaItems: z .array( z.object({ key: z.string().describe('S3 media key from get_upload_urls'), type: z.enum(['IMAGE', 'VIDEO']), sortOrder: z.number().int().min(0), coverImageKey: z.string().optional().describe('S3 key of a custom cover/thumbnail image for video posts (upload via get_upload_urls first). Supported on Instagram Reels, Facebook Reels, Pinterest video pins.'), coverTimestamp: z.string().optional().describe('Video cover timestamp in milliseconds (e.g. "5000" = 5s). Fallback when coverImageKey is also provided.'), }), ) .optional() .describe('Media attachments'), scheduledAt: z .string() .optional() .describe( 'Schedule time (ISO 8601). Required when status is SCHEDULED.', ), socialMediaId: z .string() .uuid() .describe('Target social account ID (from list_accounts)'), }), ) .min(1) .max(15) .describe('Array of posts to create'), ), status: z .enum(['DRAFT', 'SCHEDULED']) .default('SCHEDULED') .describe('Post status. SCHEDULED requires scheduledAt on all posts.'), approvalStatus: z .enum(['PENDING_APPROVAL', 'APPROVED']) .default('APPROVED') .describe('Approval workflow status'), controls: jsonParse( z.object({ // X/Twitter xRetweetUrl: z.string().optional(), // TikTok tiktokPrivacy: z.enum(['PUBLIC', 'MUTUAL_FRIENDS', 'FOLLOWER_OF_CREATOR', 'ONLY_ME']).optional(), tiktokIsDraft: z.boolean().optional(), tiktokAllowComments: z.boolean().optional(), tiktokAllowDuet: z.boolean().optional(), tiktokAllowStitch: z.boolean().optional(), tiktokBrandOrganic: z.boolean().optional(), tiktokBrandContent: z.boolean().optional(), tiktokAutoAddMusic: z.boolean().optional(), tiktokIsAigc: z.boolean().optional().describe('Declare video as AI-generated content'), // Instagram instagramPostToGrid: z.boolean().optional(), instagramPublishType: z.enum(['TIMELINE', 'STORY', 'REEL']).optional(), instagramCollaborators: z.array(z.string()).optional(), // YouTube youtubePrivacy: z.enum(['PUBLIC', 'PRIVATE', 'UNLISTED']).optional(), youtubeTags: z.array(z.string()).optional(), youtubeCategoryId: z.string().optional(), youtubeIsShort: z.boolean().optional(), youtubeMadeForKids: z.boolean().optional(), youtubeTitle: z.string().optional(), youtubePlaylistId: z.string().optional(), youtubeThumbnailKey: z.string().optional().describe('S3 media key for custom YouTube video thumbnail (image, max 2MB, min 640px wide, 1280x720 recommended)'), // Facebook facebookContentType: z.enum(['POST', 'REEL', 'STORY']).optional(), facebookAllowComments: z.boolean().optional(), facebookPrivacy: z .enum(['PUBLIC', 'FRIENDS_OF_FRIENDS', 'FRIENDS', 'SELF']) .optional(), facebookCarouselMainLink: z.string().optional(), facebookCarouselShowEndCard: z.boolean().optional(), facebookReelsCoverImageKey: z.string().optional(), facebookReelsCollaborators: z.array(z.string()).optional(), // Google Business Profile gbpLocationId: z.string().optional().describe('GBP location resource name (from list_gbp_locations)'), gbpTopicType: z.enum(['STANDARD', 'EVENT', 'OFFER']).optional().describe('Post type'), gbpCallToActionType: z.enum(['BOOK', 'ORDER', 'LEARN_MORE', 'SIGN_UP', 'CALL', 'SHOP']).optional(), gbpCallToActionUrl: z.string().optional().describe('CTA button URL (not needed for CALL, ignored for OFFER)'), gbpEventTitle: z.string().optional().describe('Title for EVENT/OFFER posts (max 58 chars)'), gbpEventStartDate: z.string().optional().describe('Start date for EVENT/OFFER (ISO 8601)'), gbpEventEndDate: z.string().optional().describe('End date for EVENT/OFFER (ISO 8601)'), gbpOfferCouponCode: z.string().optional().describe('Coupon code (OFFER only)'), gbpOfferRedeemUrl: z.string().optional().describe('Redemption URL (OFFER only)'), gbpOfferTerms: z.string().optional().describe('Terms and conditions (OFFER only)'), // Pinterest pinterestBoardId: z.string().optional(), pinterestLink: z.string().optional(), // LinkedIn linkedinAttachmentKey: z.string().optional(), linkedinAttachmentTitle: z.string().optional(), }) .optional() .describe('Platform-specific controls (shared across all posts in the batch)'), ), }, - src/tools/posts.ts:82-199 (registration)Registration of the 'create_posts' tool using server.tool() on line 82, inside the registerPostTools function. The tool is connected to the McpServer instance, which is invoked from src/index.ts line 17.
server.tool( 'create_posts', 'Create and schedule social media posts. Supports batch creation (up to 15 posts). Each post targets a specific social account.', { posts: jsonParse( z .array( z.object({ content: z.string().describe('Post text content'), firstComment: z.string().optional().describe('First comment to add after publishing'), mediaItems: z .array( z.object({ key: z.string().describe('S3 media key from get_upload_urls'), type: z.enum(['IMAGE', 'VIDEO']), sortOrder: z.number().int().min(0), coverImageKey: z.string().optional().describe('S3 key of a custom cover/thumbnail image for video posts (upload via get_upload_urls first). Supported on Instagram Reels, Facebook Reels, Pinterest video pins.'), coverTimestamp: z.string().optional().describe('Video cover timestamp in milliseconds (e.g. "5000" = 5s). Fallback when coverImageKey is also provided.'), }), ) .optional() .describe('Media attachments'), scheduledAt: z .string() .optional() .describe( 'Schedule time (ISO 8601). Required when status is SCHEDULED.', ), socialMediaId: z .string() .uuid() .describe('Target social account ID (from list_accounts)'), }), ) .min(1) .max(15) .describe('Array of posts to create'), ), status: z .enum(['DRAFT', 'SCHEDULED']) .default('SCHEDULED') .describe('Post status. SCHEDULED requires scheduledAt on all posts.'), approvalStatus: z .enum(['PENDING_APPROVAL', 'APPROVED']) .default('APPROVED') .describe('Approval workflow status'), controls: jsonParse( z.object({ // X/Twitter xRetweetUrl: z.string().optional(), // TikTok tiktokPrivacy: z.enum(['PUBLIC', 'MUTUAL_FRIENDS', 'FOLLOWER_OF_CREATOR', 'ONLY_ME']).optional(), tiktokIsDraft: z.boolean().optional(), tiktokAllowComments: z.boolean().optional(), tiktokAllowDuet: z.boolean().optional(), tiktokAllowStitch: z.boolean().optional(), tiktokBrandOrganic: z.boolean().optional(), tiktokBrandContent: z.boolean().optional(), tiktokAutoAddMusic: z.boolean().optional(), tiktokIsAigc: z.boolean().optional().describe('Declare video as AI-generated content'), // Instagram instagramPostToGrid: z.boolean().optional(), instagramPublishType: z.enum(['TIMELINE', 'STORY', 'REEL']).optional(), instagramCollaborators: z.array(z.string()).optional(), // YouTube youtubePrivacy: z.enum(['PUBLIC', 'PRIVATE', 'UNLISTED']).optional(), youtubeTags: z.array(z.string()).optional(), youtubeCategoryId: z.string().optional(), youtubeIsShort: z.boolean().optional(), youtubeMadeForKids: z.boolean().optional(), youtubeTitle: z.string().optional(), youtubePlaylistId: z.string().optional(), youtubeThumbnailKey: z.string().optional().describe('S3 media key for custom YouTube video thumbnail (image, max 2MB, min 640px wide, 1280x720 recommended)'), // Facebook facebookContentType: z.enum(['POST', 'REEL', 'STORY']).optional(), facebookAllowComments: z.boolean().optional(), facebookPrivacy: z .enum(['PUBLIC', 'FRIENDS_OF_FRIENDS', 'FRIENDS', 'SELF']) .optional(), facebookCarouselMainLink: z.string().optional(), facebookCarouselShowEndCard: z.boolean().optional(), facebookReelsCoverImageKey: z.string().optional(), facebookReelsCollaborators: z.array(z.string()).optional(), // Google Business Profile gbpLocationId: z.string().optional().describe('GBP location resource name (from list_gbp_locations)'), gbpTopicType: z.enum(['STANDARD', 'EVENT', 'OFFER']).optional().describe('Post type'), gbpCallToActionType: z.enum(['BOOK', 'ORDER', 'LEARN_MORE', 'SIGN_UP', 'CALL', 'SHOP']).optional(), gbpCallToActionUrl: z.string().optional().describe('CTA button URL (not needed for CALL, ignored for OFFER)'), gbpEventTitle: z.string().optional().describe('Title for EVENT/OFFER posts (max 58 chars)'), gbpEventStartDate: z.string().optional().describe('Start date for EVENT/OFFER (ISO 8601)'), gbpEventEndDate: z.string().optional().describe('End date for EVENT/OFFER (ISO 8601)'), gbpOfferCouponCode: z.string().optional().describe('Coupon code (OFFER only)'), gbpOfferRedeemUrl: z.string().optional().describe('Redemption URL (OFFER only)'), gbpOfferTerms: z.string().optional().describe('Terms and conditions (OFFER only)'), // Pinterest pinterestBoardId: z.string().optional(), pinterestLink: z.string().optional(), // LinkedIn linkedinAttachmentKey: z.string().optional(), linkedinAttachmentTitle: z.string().optional(), }) .optional() .describe('Platform-specific controls (shared across all posts in the batch)'), ), }, async (input) => { const data = await client.post<{ postIds: string[] }>('/social-posts', { posts: input.posts, status: input.status, approvalStatus: input.approvalStatus, controls: input.controls, }); return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }], }; }, ); - src/index.ts:17-17 (registration)Top-level registration: registerPostTools(server, client) is called in the main entry point, which wires up the create_posts tool to the MCP server.
registerPostTools(server, client); - src/tools/posts.ts:22-34 (helper)The jsonParse helper function used to preprocess input values. If the value is a string (e.g., because some MCP clients stringify complex params), it attempts to JSON.parse it before passing to the Zod schema for validation.
/** Some MCP clients stringify complex params — parse them back before validation. */ function jsonParse<T extends z.ZodTypeAny>(schema: T) { return z.preprocess((val) => { if (typeof val === 'string') { try { return JSON.parse(val); } catch { return val; } } return val; }, schema); }