Skip to main content
Glama

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

TableJSON Schema
NameRequiredDescriptionDefault
postsYesArray of posts to create
statusNoPost status. SCHEDULED requires scheduledAt on all posts.SCHEDULED
approvalStatusNoApproval workflow statusAPPROVED
controlsYesPlatform-specific controls (shared across all posts in the batch)

Implementation Reference

  • 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) }],
        };
      },
    );
  • 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)'),
      ),
    },
  • 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);
  • 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);
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description must fully disclose behavior. It only says 'create and schedule' but omits side effects, permission requirements, rate limits, or what happens on failure. The mutation nature is implied but not detailed.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences, no redundancy, front-loaded with key information. Every word earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (many nested controls, no output schema), the description is insufficient. It fails to explain workflow dependencies (e.g., media upload via get_upload_urls), the role of status/approval, or the return value. Much needed context is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema documents all parameters. The description adds no meaningful value beyond the schema; it restates batch limits already present. Thus baseline 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool creates and schedules social media posts, specifies batch support (up to 15), and notes each post targets a specific account. It effectively distinguishes from listing and deletion siblings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage for creating posts but does not explicitly state when to use versus alternatives, nor provides exclusionary hints. No guidance on prerequisites like needing to upload media first.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/peturgeorgievv-factory/postfast-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server