{"serverId":"mcp-registry/com.viral-cat/viral-cat","tools":[{"name":"cancel_post","fixed":{"name":"cancel_post","description":"WRITE (safe): cancel a queued post, or move it to a new slot. Omit scheduled_date to CANCEL (status -> cancelled; the publisher ignores it). Pass scheduled_date to RESCHEDULE to that time instead (ISO 8601, at least 15 minutes out; a draft keeps its draft status). Works on draft or scheduled posts (cancel or reschedule); also works on FAILED posts but cancel-only (omit scheduled_date) — this is the way to clear a dead failed-post backlog, since failed has no other exit. Posts already posting/pos","inputSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","required":["post_id"],"properties":{"post_id":{"type":"integer","maximum":9007199254740991,"minimum":-9007199254740991,"description":"Post id to cancel or reschedule (from content_queue)"},"scheduled_date":{"type":"string","description":"Omit to cancel. Provide to reschedule: ISO 8601, at least 15 minutes in the future"}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"idempotentHint":true}},"changes":[{"kind":"annotations","detail":"Added readOnlyHint: false, destructiveHint: true, idempotentHint: true (inferred from the name).","verify":true}],"tokensBefore":264,"tokensAfter":264},{"name":"create_brand","fixed":{"name":"create_brand","description":"WRITE (gated): create a brand (content profile) so create_draft and schedule_post have something to target. Every other write here needs a brand_id, so an agent had no way to set one up. Returns the brand_id and the Drive folder the publisher will read its media from (Viral-Cat/_Res/{name}/Vids). Publishes nothing and reaches no audience.","inputSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","required":["name"],"properties":{"name":{"type":"string","maxLength":100,"minLength":1,"description":"Brand name. Also names its Drive media folder, so keep it filesystem-friendly."},"cta_url":{"type":"string","maxLength":500,"description":"Link included in posts."},"hashtags":{"type":"array","items":{"type":"string","maxLength":100},"maxItems":30,"description":"Brand hashtags appended to captions unless skip_hashtags is set."},"description":{"type":"string","maxLength":1000},"website_url":{"type":"string","maxLength":500},"content_type":{"enum":["video","image"],"type":"string","description":"What the publisher should expect to find for this brand."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false}},"changes":[{"kind":"annotations","detail":"Added readOnlyHint: false, destructiveHint: false, idempotentHint: false (inferred from the name).","verify":true}],"tokensBefore":313,"tokensAfter":313},{"name":"create_draft","fixed":{"name":"create_draft","description":"WRITE (safe): create a DRAFT post for a brand. Drafts are never picked up by the publisher, so nothing goes live; a human approves in the dashboard or promotes it later with schedule_post. scheduled_date is a proposed slot (default: 7 days out).","inputSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","required":["brand_id","platform"],"properties":{"caption":{"type":"string","maxLength":2200,"description":"Post body. Omit to inherit the brand's taglines."},"brand_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"platform":{"type":"string","maxLength":20,"description":"Target platform, e.g. youtube, tiktok, instagram"},"reply_text":{"type":"string","maxLength":2200,"description":"Follow-up reply posted under the main post. On X this is where a link belongs: a URL in the post body costs up to 94% of the reach (near-total on a non-Premium account), while the same URL one reply down costs nothing. Omit to reply with the brand's CTA url; pass an empty string for no reply at all. Ignored by every platform except X."},"skip_hashtags":{"type":"boolean","description":"true = publish the caption as-is with NO brand hashtags appended; false = always append them. Omit to auto-detect: long hand-authored captions (>280 chars, no '#') skip hashtags. The result's hashtagsSkipped shows the decision."},"scheduled_date":{"type":"string","description":"Proposed ISO 8601 slot, e.g. 2026-07-15T09:00:00Z"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false}},"changes":[{"kind":"annotations","detail":"Added readOnlyHint: false, destructiveHint: false, idempotentHint: false (inferred from the name).","verify":true}],"tokensBefore":457,"tokensAfter":457},{"name":"create_media_upload","fixed":{"name":"create_media_upload","description":"WRITE (gated): open a Google Drive upload session for media you hold locally, so a clip rendered on your own machine never has to be published publicly just so ViralCat can fetch it back. Returns an uploadUrl - PUT the bytes to it yourself (no auth header) and Drive stores the file; the response Location/id is the fileId for create_draft or schedule_post. When brand is given the file lands in the folder the generator actually reads for that media type: Viral-Cat/_Res/{brand}/Imgs for an image, V","inputSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","required":["file_name","mime_type","size_bytes"],"properties":{"kind":{"enum":["video","image","music"],"type":"string","description":"Which brand folder receives the file: Vids, Imgs or Music. Omit to route by media type, which is almost always right."},"brand":{"type":"string","maxLength":200,"description":"Brand whose _Res/{brand} folder should receive the file. Ignored when folder_id is given."},"file_name":{"type":"string","maxLength":200,"description":"Name to save as in Drive, e.g. \"ducktax-short.mp4\"."},"folder_id":{"type":"string","maxLength":200,"description":"Destination Drive folder id, overriding brand."},"mime_type":{"type":"string","maxLength":100,"description":"Media type of the file, e.g. \"video/mp4\"."},"size_bytes":{"type":"integer","maximum":9007199254740991,"description":"Exact byte length of the file. Drive rejects a mismatch.","exclusiveMinimum":0}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false}},"changes":[{"kind":"annotations","detail":"Added readOnlyHint: false, destructiveHint: false, idempotentHint: false (inferred from the name).","verify":true}],"tokensBefore":423,"tokensAfter":423},{"name":"get_brand_details","fixed":{"name":"get_brand_details","description":"Full content profile for one brand: description, taglines, hashtags, CTA/website URL, niche (category/subcategory), content type, target platforms, and active discount. Use this to write on-brand captions BEFORE calling create_draft or schedule_post. Read-only; returns no secrets.","inputSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","required":["brand_id"],"properties":{"brand_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"include_samples":{"type":"boolean","default":false,"description":"Include sample users (email contains \"sample\"). Default: real data only."}}},"annotations":{"readOnlyHint":true}},"changes":[{"kind":"annotations","detail":"Added readOnlyHint: true (inferred from the name).","verify":true}],"tokensBefore":227,"tokensAfter":227},{"name":"get_pipeline_summary","fixed":{"name":"get_pipeline_summary","description":"High-level snapshot of the posting pipeline: active brand count, a status breakdown (how many scheduled/posted/failed/etc.), a per-platform breakdown, how many posts are due in the next 7 days, and the next scheduled date. Use for 'how's the pipeline?' style questions.","inputSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","properties":{"include_samples":{"type":"boolean","default":false,"description":"Include sample users (email contains \"sample\"). Default: real data only."}}},"annotations":{"readOnlyHint":true}},"changes":[{"kind":"annotations","detail":"Added readOnlyHint: true (inferred from the name).","verify":true}],"tokensBefore":156,"tokensAfter":156},{"name":"list_brands","fixed":{"name":"list_brands","description":"List brands (content profiles), newest first, with per-brand post counts (posted/scheduled) and the next scheduled date. Use to see which brands exist and how active each is. Excludes archived brands. Returns `total` (all active brands) next to `count` (rows returned): when `truncated` is true the roster was cut off by `limit`, so raise it (max 50) before concluding a brand does not exist.","inputSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","properties":{"limit":{"type":"integer","default":20,"maximum":50,"minimum":1},"include_samples":{"type":"boolean","default":false,"description":"Include sample users (email contains \"sample\"). Default: real data only."}}},"annotations":{"readOnlyHint":true}},"changes":[{"kind":"annotations","detail":"Added readOnlyHint: true (inferred from the name).","verify":true}],"tokensBefore":206,"tokensAfter":206},{"name":"list_connections","fixed":{"name":"list_connections","description":"List connected platform accounts (channels) and their health: platform, account username, connected flag, posting-enabled flag, and token expiry. Use to check which platforms are wired up and whether any connection has expired. Never returns access or refresh tokens.","inputSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","properties":{"include_samples":{"type":"boolean","default":false,"description":"Include sample users (email contains \"sample\"). Default: real data only."}}},"annotations":{"readOnlyHint":true}},"changes":[{"kind":"annotations","detail":"Added readOnlyHint: true (inferred from the name).","verify":true}],"tokensBefore":154,"tokensAfter":154},{"name":"list_posts","fixed":{"name":"list_posts","description":"List posts across all platforms, newest scheduled first. Each row is one publication on one platform. Filter by status (draft|scheduled|posting|posted|failed|cancelled) and/or platform (youtube|tiktok|instagram|...). Returns caption, brand name, permalink, error message when present, and publishWarning for posts that succeeded with an anomaly (e.g. published without media).","inputSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","properties":{"limit":{"type":"integer","default":20,"maximum":50,"minimum":1},"status":{"enum":["draft","scheduled","posting","posted","failed","cancelled"],"type":"string"},"platform":{"type":"string","maxLength":20,"description":"Filter to a single platform, e.g. youtube or tiktok"},"include_samples":{"type":"boolean","default":false,"description":"Include sample users (email contains \"sample\"). Default: real data only."}}},"annotations":{"readOnlyHint":true}},"changes":[{"kind":"annotations","detail":"Added readOnlyHint: true (inferred from the name).","verify":true}],"tokensBefore":259,"tokensAfter":259},{"name":"post_performance","fixed":{"name":"post_performance","description":"Publish-outcome metrics over a rolling window (default 30 days): posted/failed/cancelled totals, success rate, per-platform breakdown, and the most recent published posts with permalinks plus their latest stored engagement snapshot (views/likes/comments/shares; available for youtube/tiktok/twitter/threads). Snapshots are stored, not live: call refresh_post_metrics first if the numbers must be current. Optionally scope by brand_id and/or platform.","inputSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","properties":{"days":{"type":"integer","default":30,"maximum":365,"minimum":1},"brand_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"platform":{"type":"string","maxLength":20},"include_samples":{"type":"boolean","default":false,"description":"Include sample users (email contains \"sample\"). Default: real data only."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false}},"changes":[{"kind":"annotations","detail":"Added readOnlyHint: false, destructiveHint: false, idempotentHint: false (inferred from the name).","verify":true}],"tokensBefore":298,"tokensAfter":298},{"name":"preflight_check","fixed":{"name":"preflight_check","description":"READ-ONLY: validate a post BEFORE it is scheduled or published, so a problem is caught up front instead of discovered as a failure afterwards. Checks the exact things that produced the failed-post backlog: incompatible media for the platform (e.g. video to Pinterest), a disconnected or unrefreshable platform token (LinkedIn has no refresh token on this API tier and needs a manual browser reconnect roughly every 60 days — that's an expected state, not a bug), Facebook posting to an account with n","inputSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","properties":{"limit":{"type":"integer","default":20,"maximum":50,"minimum":1,"description":"Queue-scan mode only: how many upcoming posts to check."},"post_id":{"type":"integer","maximum":9007199254740991,"minimum":-9007199254740991,"description":"Check one existing post (id from list_posts or content_queue). Omit to scan the whole upcoming queue instead."},"include_samples":{"type":"boolean","default":false,"description":"Include sample users (email contains \"sample\"). Default: real data only."}}},"annotations":{"readOnlyHint":true}},"changes":[{"kind":"annotations","detail":"Added readOnlyHint: true (inferred from the name).","verify":true}],"tokensBefore":315,"tokensAfter":315},{"name":"refresh_post_metrics","fixed":{"name":"refresh_post_metrics","description":"WRITE (gated): fetch current engagement (views/likes/comments/shares) for already-published posts and store today's snapshot, then read it back with post_performance. This is the only way to update the numbers without opening the dashboard Stats tab. Supported platforms: youtube, tiktok, twitter, threads; LinkedIn and the rest expose no engagement API. A 60-minute cooldown applies per account and protects X's free-tier read quota, so a call inside it returns skipped:true and changes nothing. Pub","inputSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","properties":{"include_samples":{"type":"boolean","default":false,"description":"Include sample users. Default false: real accounts only, matching every read tool."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false}},"changes":[{"kind":"annotations","detail":"Added readOnlyHint: false, destructiveHint: false, idempotentHint: false (inferred from the name).","verify":true}],"tokensBefore":222,"tokensAfter":222},{"name":"reschedule_post","fixed":{"name":"reschedule_post","description":"WRITE (gated): move ONE post to a new future slot (ISO 8601, at least 15 minutes out). Unlike cancel_post this also accepts FAILED posts: a failed post is re-queued (status -> scheduled, error cleared). Draft and scheduled posts keep their status. Refuses posts that would structurally fail again (archived brand, incompatible media, Facebook with no Page).","inputSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","required":["post_id","scheduled_date"],"properties":{"post_id":{"type":"integer","maximum":9007199254740991,"minimum":-9007199254740991,"description":"Post id (from list_posts, content_queue or diagnose_failures)"},"scheduled_date":{"type":"string","description":"New slot, ISO 8601, at least 15 minutes in the future"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false}},"changes":[{"kind":"annotations","detail":"Added readOnlyHint: false, destructiveHint: false, idempotentHint: false (inferred from the name).","verify":true}],"tokensBefore":224,"tokensAfter":224},{"name":"retry_failed_posts","fixed":{"name":"retry_failed_posts","description":"WRITE (gated): bulk re-queue recoverable failed posts (default reason classes: token_expired, missed_window, platform_transient_error, drive_media_missing). dry_run DEFAULTS TO TRUE and only returns the plan; pass dry_run=false to actually reschedule. Retries are staggered per platform (spacing_minutes apart) and capped at limit, so a bulk retry cannot dogpile a platform API. Posts that would structurally fail again (archived brand, incompatible media, Facebook with no Page) are skipped with the","inputSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","properties":{"limit":{"type":"integer","default":20,"maximum":50,"minimum":1},"dry_run":{"type":"boolean","default":true,"description":"true = plan only, change nothing (the default)"},"reasons":{"type":"array","items":{"type":"string","maxLength":40},"description":"Failure reason classes to retry. Default: token_expired, missed_window, platform_transient_error, drive_media_missing. Non-retryable classes are rejected."},"platform":{"type":"string","maxLength":20,"description":"Scope to a single platform"},"start_at":{"type":"string","description":"ISO 8601 start of the retry window, at least 15 minutes out. Default: ~20 minutes from now."},"spacing_minutes":{"type":"integer","default":30,"maximum":240,"minimum":5,"description":"Gap between retries on the same platform"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false}},"changes":[{"kind":"annotations","detail":"Added readOnlyHint: false, destructiveHint: false, idempotentHint: false (inferred from the name).","verify":true}],"tokensBefore":393,"tokensAfter":393},{"name":"schedule_post","fixed":{"name":"schedule_post","description":"WRITE (gated): queue a post for FUTURE publishing, at least 15 minutes out (immediate posting is not available over MCP). Either pass post_id to promote an existing draft, or brand_id + platform to create a new scheduled post. The publishing cron sends it at the scheduled time.","inputSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","required":["scheduled_date"],"properties":{"caption":{"type":"string","maxLength":2200},"post_id":{"type":"integer","maximum":9007199254740991,"minimum":-9007199254740991,"description":"Draft post id to promote (from content_queue)"},"brand_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"platform":{"type":"string","maxLength":20},"reply_text":{"type":"string","maxLength":2200,"description":"Follow-up reply posted under the main post. On X this is where a link belongs: a URL in the post body costs up to 94% of the reach (near-total on a non-Premium account), while the same URL one reply down costs nothing. Omit to reply with the brand's CTA url; pass an empty string for no reply at all. Ignored by every platform except X."},"skip_hashtags":{"type":"boolean","description":"true = publish the caption as-is with NO brand hashtags appended; false = always append them. Omit to auto-detect: long hand-authored captions (>280 chars, no '#') skip hashtags. When promoting a draft without a new caption, omitting this keeps the draft's existing setting."},"scheduled_date":{"type":"string","description":"ISO 8601, at least 15 minutes in the future"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false}},"changes":[{"kind":"annotations","detail":"Added readOnlyHint: false, destructiveHint: false, idempotentHint: false (inferred from the name).","verify":true}],"tokensBefore":481,"tokensAfter":481},{"name":"update_brand","fixed":{"name":"update_brand","description":"WRITE (gated): correct a brand's content profile: description, taglines, hashtags, CTA and website links, content type, and the auto-post switch. This is the repair path for a brand carrying wrong copy, which otherwise makes every caption for it wrong, and the off switch for a brand whose generator is posting against your content rules. Only the fields you pass change; the rest keep their stored values. Cannot rename a brand (the name also names its Drive media folder) and cannot touch posting f","inputSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","required":["brand_id"],"properties":{"cta_url":{"type":"string","maxLength":500,"description":"Link included in posts. Pass an empty string to clear it."},"brand_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"hashtags":{"type":"array","items":{"type":"string","maxLength":100},"maxItems":30,"description":"Brand hashtags appended to captions unless skip_hashtags is set."},"taglines":{"type":"array","items":{"type":"string","maxLength":500},"maxItems":30,"description":"Short lines the publisher rotates through when a post has no caption of its own."},"description":{"type":"string","maxLength":1000,"description":"What the product actually is. The generator writes captions from this, so a wrong description makes every post wrong."},"website_url":{"type":"string","maxLength":500},"content_type":{"enum":["video","image"],"type":"string","description":"What the publisher should expect to find for this brand."},"auto_post_enabled":{"type":"boolean","description":"false = stop the content generator booking posts for this brand from its next cycle; true = let it resume. This is the only way to stop a brand auto-posting without opening the dashboard. Already-scheduled posts are NOT withdrawn by this: cancel those with cancel_post."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true}},"changes":[{"kind":"annotations","detail":"Added readOnlyHint: false, destructiveHint: false, idempotentHint: true (inferred from the name).","verify":true}],"tokensBefore":567,"tokensAfter":567},{"name":"upload_media","fixed":{"name":"upload_media","description":"WRITE (gated): fetch a public media URL and store it in the connected Google Drive, returning the Drive fileId to pass to create_draft or schedule_post. This uploads to YOUR OWN Drive; it publishes nothing and reaches no audience. Accepts video, image or audio up to 100MB. With brand, files land in the folder the generator actually reads for that media type: Viral-Cat/_Res/{brand}/Imgs for an image, Vids for a video, Music for audio. Without brand it defaults to the Viral-Cat folder, and folder_","inputSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","required":["source_url"],"properties":{"kind":{"enum":["video","image","music"],"type":"string","description":"Which brand folder receives the file: Vids, Imgs or Music. Omit to route by media type, which is almost always right."},"brand":{"type":"string","maxLength":200,"description":"Brand whose _Res/{brand} folder should receive the file. Ignored when folder_id is given."},"file_name":{"type":"string","maxLength":200,"description":"Name to save as. Omit to take it from the URL."},"folder_id":{"type":"string","maxLength":200,"description":"Destination Drive folder id, overriding brand. Omit to use the Viral-Cat folder."},"source_url":{"type":"string","format":"uri","description":"Public https URL of the media to store, e.g. a rendered Short. Must be reachable without auth."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false}},"changes":[{"kind":"annotations","detail":"Added readOnlyHint: false, destructiveHint: false, idempotentHint: false (inferred from the name).","verify":true}],"tokensBefore":393,"tokensAfter":393}],"toolsChanged":17,"tokensSaved":0,"todo":[{"tool":"campaign_summary","params":["brand_id"]},{"tool":"content_queue","params":["limit"]},{"tool":"create_brand","params":["description","website_url"]},{"tool":"create_draft","params":["brand_id"]},{"tool":"get_brand_details","params":["brand_id"]},{"tool":"list_brands","params":["limit"]},{"tool":"list_posts","params":["limit","status"]},{"tool":"post_performance","params":["days","brand_id","platform"]},{"tool":"retry_failed_posts","params":["limit"]},{"tool":"schedule_post","params":["caption","brand_id","platform"]},{"tool":"update_brand","params":["brand_id","website_url"]}],"note":"Deterministic fixes. Changes marked verify need a human look before shipping (renames, inferred annotations, inferred types, removed text). Parameter descriptions in `todo` need to be written by someone who knows the API."}