authorizeCurrentWorkspace( $request, 'createPost', 'Not authorized to create posts.', ); if (! $workspace instanceof Workspace) { return $workspace; } $validated = $request->validate( [ 'content' => ['nullable', 'string', 'max:10000'], 'scheduled_at' => ['nullable', 'date', 'after:now'], 'label_ids' => ['sometimes', 'array'], 'label_ids.*' => ['uuid', Rule::exists('workspace_labels', 'id')->where('workspace_id', $workspace->id)], 'platforms' => ['sometimes', 'array'], 'platforms.*.social_account_id' => [ 'required', 'uuid', Rule::exists('social_accounts', 'id') ->where('workspace_id', $workspace->id) ->where('is_active', true), ], 'platforms.*.content_type' => ['required', 'string', Rule::in(array_column(ContentType::cases(), 'value')), new ContentTypeMatchesPlatform], ...PostPlatformMetaRules::rules(), ], PostPlatformMetaRules::messages(), PostPlatformMetaRules::attributes(), ); $validated['created_via'] = CreatedVia::Mcp; $post = CreatePost::execute($workspace, $request->user(), $validated); $post->load(['postPlatforms.socialAccount', 'labels']); return Response::structured((new PostResource($post))->resolve()); } public function schema(JsonSchema $schema): array { return [ 'content' => $schema->string()->description('The post caption/text body. Optional — can be edited later.'), 'scheduled_at' => $schema->string()->description('Optional ISO 8601 datetime in the future (e.g. 2026-05-10T15:30:00Z). Omit it or pass null to create an unscheduled draft.'), 'label_ids' => $schema->array() ->items($schema->string()) ->description('Workspace label IDs to attach to the post.'), 'platforms' => $schema->array() ->items($schema->object(fn ($p) => [ 'social_account_id' => $p->string()->required()->description('UUID of the connected social account.'), 'content_type' => $p->string()->required()->description('Format for this platform (e.g. linkedin_post, x_post, instagram_feed).'), 'meta' => $p->object()->description('Per-platform metadata. Instagram/Facebook: aspect_ratio (1:1|4:5|16:9|original). TikTok: privacy_level (required to publish) + flags (allow_comments, allow_duet, allow_stitch, disclose, brand_content_toggle, brand_organic_toggle, is_aigc, auto_add_music). Pinterest: board_id (required to publish — call ListPinterestBoardsTool first), title (≤100), link (destination URL). Pin description comes from the post content. Discord: channel_id (required to publish — call ListDiscordChannelsTool first), mentions ([{token,label}]), embeds ([{title,description,url,image,color}]).'), ])) ->description('Platforms to publish on. Accounts not listed remain available but disabled.'), ]; } }