Skip to content

Content Management Examples ​

Practical examples for managing content, media, and profiles on AT Protocol.

These examples show how an LLM interacts with the AT Protocol MCP Server tools using JSON-formatted parameters.

Response shapes are illustrative

Tool results are returned as stringified JSON text content, not a guaranteed structured schema. The Response (JSON) blocks below illustrate the kind of data a tool returns; exact field names and structure may differ.

Rate limiting and batching

Each tool is rate limited to roughly 100 requests per minute per tool. When a workflow performs many calls in sequence (uploads, deletions, posts), pace them out with a short delay between calls so you stay under the limit. The per-step "wait N seconds" notes below are conservative spacing suggestions, not hard requirements.

Profile Management ​

Update Profile Information ​

User Request:

"Update my profile display name to 'Alice Smith' and bio to 'Software engineer and coffee enthusiast ☕ Building cool things with AT Protocol'"

Tool Call: update_profile

Parameters (JSON):

json
{
  "displayName": "Alice Smith",
  "description": "Software engineer and coffee enthusiast ☕\nBuilding cool things with AT Protocol"
}

Response (JSON):

json
{
  "success": true,
  "message": "Profile updated successfully"
}

Update Profile Avatar ​

User Request:

"Update my profile avatar with this image"

Workflow:

Step 1: Upload the Image

Tool Call: upload_image

Parameters (JSON):

json
{
  "filePath": "./images/avatar.jpg",
  "altText": "Profile avatar"
}

Step 2: Update the Profile

Tool Call: update_profile — pass the image.blob object returned by upload_image verbatim:

Parameters (JSON):

json
{
  "avatar": {
    "type": "blob",
    "ref": "bafkreiavatar123...",
    "mimeType": "image/jpeg",
    "size": 145678
  }
}

Note: avatar and banner take a pre-uploaded blob descriptor (the image.blob from a prior upload_image call), not raw image data.

Update Profile Banner ​

User Request:

"Update my profile banner"

Workflow: Upload the banner image with upload_image (as above), then:

Tool Call: update_profile

Parameters (JSON):

json
{
  "banner": {
    "type": "blob",
    "ref": "bafkreibanner456...",
    "mimeType": "image/jpeg",
    "size": 398765
  }
}

Complete Profile Update ​

User Request:

"Update my entire profile with new name, bio, avatar, and banner"

Workflow: Upload the avatar and banner images with upload_image first, then:

Tool Call: update_profile

Parameters (JSON):

json
{
  "displayName": "Alice Smith",
  "description": "Full-stack developer | Open source contributor | Coffee addict",
  "avatar": {
    "type": "blob",
    "ref": "bafkreiavatar123...",
    "mimeType": "image/jpeg",
    "size": 145678
  },
  "banner": {
    "type": "blob",
    "ref": "bafkreibanner456...",
    "mimeType": "image/jpeg",
    "size": 398765
  }
}

Media Upload ​

Upload Single Image ​

User Request:

"Upload this sunset photo with alt text 'A beautiful sunset over the ocean'"

Tool Call: upload_image

Parameters (JSON):

json
{
  "filePath": "./photos/sunset.jpg",
  "altText": "A beautiful sunset over the ocean"
}

Response (JSON):

json
{
  "success": true,
  "message": "Image uploaded successfully from ./photos/sunset.jpg",
  "image": {
    "blob": {
      "type": "blob",
      "ref": "bafkreiabc123...",
      "mimeType": "image/jpeg",
      "size": 245678
    },
    "alt": "A beautiful sunset over the ocean"
  }
}

Note: upload_image reads a local file (JPEG, PNG, GIF, WebP, or AVIF; max 1 MB) whose path must resolve within the allowed media directory (ATPROTO_MEDIA_DIR, defaults to the working directory). Pass the returned image.blob object verbatim as embed.images[].image in create_post, or as avatar/banner in update_profile.

Upload Multiple Images Workflow ​

User Request:

"Upload these three photos"

Workflow: Call upload_image for each image sequentially

First Image:

json
{
  "filePath": "./photos/photo1.jpg",
  "altText": "Image 1"
}

Second Image:

json
{
  "filePath": "./photos/photo2.jpg",
  "altText": "Image 2"
}

Third Image:

json
{
  "filePath": "./photos/photo3.jpg",
  "altText": "Image 3"
}

Result: Collect the returned image.blob descriptors to use in a post (up to 4 per post).

Upload Video ​

User Request:

"Upload this tutorial video"

Tool Call: upload_video

Parameters (JSON):

json
{
  "filePath": "./videos/tutorial.mp4",
  "altText": "Tutorial on using AT Protocol"
}

Response (JSON):

json
{
  "success": true,
  "message": "Video uploaded and processed successfully from ./videos/tutorial.mp4",
  "video": {
    "blob": {
      "type": "blob",
      "ref": "bafkreivideo123...",
      "mimeType": "video/mp4",
      "size": 5242880
    },
    "alt": "Tutorial on using AT Protocol",
    "jobId": "rmpdzv4uoctlginpv3oddi6w"
  }
}

Note: upload_video reads a local file (MP4, MOV, or WebM; max 100 MB), checks your daily video-upload quota, uploads to the app.bsky.video service (video.bsky.app), and waits — up to 5 minutes — for transcoding to finish. The returned video.blob describes the processed video; pass it verbatim as embed.video.video in create_post.

Upload Video with Captions ​

User Request:

"Upload this conference talk video with English captions"

Tool Call: upload_video

Parameters (JSON):

json
{
  "filePath": "./videos/talk.mp4",
  "altText": "Conference talk with captions",
  "captions": [
    {
      "lang": "en",
      "file": "./captions/en.vtt"
    }
  ]
}

Response (JSON):

json
{
  "success": true,
  "message": "Video uploaded and processed successfully from ./videos/talk.mp4",
  "video": {
    "blob": {
      "type": "blob",
      "ref": "bafkreivideo456...",
      "mimeType": "video/mp4",
      "size": 9120004
    },
    "alt": "Conference talk with captions",
    "jobId": "kwx5ej3tqxxv2hcyvbq7worf",
    "captions": [
      {
        "lang": "en",
        "file": {
          "type": "blob",
          "ref": "bafkreicaption789...",
          "mimeType": "text/vtt",
          "size": 1843
        }
      }
    ]
  }
}

Note: Caption files are local WebVTT (.vtt) paths; each must be under 20 kB (the embed lexicon limit) or it is skipped. The captions come back as text/vtt blob descriptors ready for create_post.

Post the Uploaded Video ​

Tool Call: create_post — pass video.blob verbatim as embed.video.video and each video.captions[] entry verbatim under embed.video.captions:

Parameters (JSON):

json
{
  "text": "My conference talk is online! 🎬",
  "embed": {
    "video": {
      "video": {
        "type": "blob",
        "ref": "bafkreivideo456...",
        "mimeType": "video/mp4",
        "size": 9120004
      },
      "alt": "Conference talk with captions",
      "aspectRatio": { "width": 16, "height": 9 },
      "captions": [
        {
          "lang": "en",
          "file": {
            "type": "blob",
            "ref": "bafkreicaption789...",
            "mimeType": "text/vtt",
            "size": 1843
          }
        }
      ]
    }
  }
}

Note: A post can carry only one embed — a video cannot be combined with images, an external link, or a quote.

Rich Text Posts ​

Rich text is handled by create_post. Mentions, links, and #hashtags in the text are auto-detected into richtext facets, so for most posts you can just pass text. Supply explicit facets only when you need precise control — each feature is { "type": "mention" | "link" | "hashtag", "value": "..." }, where value is a handle/DID for a mention, the URL for a link, or the tag (without #) for a hashtag.

Post with Mentions ​

User Request:

"Create a post saying 'Great work @alice.bsky.social!'"

Workflow:

Step 1: Get User Profile (to resolve handle to DID)

Tool Call: get_user_profile

Parameters (JSON):

json
{
  "actor": "alice.bsky.social"
}

Step 2: Create the Post

Tool Call: create_post

Parameters (JSON):

json
{
  "text": "Great work @alice.bsky.social!",
  "facets": [
    {
      "index": {
        "byteStart": 11,
        "byteEnd": 30
      },
      "features": [
        {
          "type": "mention",
          "value": "alice.bsky.social"
        }
      ]
    }
  ]
}

Note: Byte positions must be calculated based on UTF-8 encoding of the text. Mention handles supplied in value are resolved to DIDs automatically. In practice you can also just pass the text and let create_post auto-detect the mention.

Post with Links ​

User Request:

"Create a post with a link: 'Check out this article: https://example.com'"

Tool Call: create_post

Parameters (JSON):

json
{
  "text": "Check out this article: https://example.com",
  "facets": [
    {
      "index": {
        "byteStart": 24,
        "byteEnd": 43
      },
      "features": [
        {
          "type": "link",
          "value": "https://example.com"
        }
      ]
    }
  ]
}

Post with Hashtags ​

User Request:

"Create a post saying 'Loving the #atproto community!'"

Tool Call: create_post

Parameters (JSON):

json
{
  "text": "Loving the #atproto community!",
  "facets": [
    {
      "index": {
        "byteStart": 11,
        "byteEnd": 19
      },
      "features": [
        {
          "type": "hashtag",
          "value": "atproto"
        }
      ]
    }
  ]
}

Post with Multiple Facets ​

User Request:

"Create a post: 'Hey @alice check out #atproto at https://atproto.com'"

Workflow:

Step 1: Resolve Mention

Tool Call: get_user_profile

Parameters (JSON):

json
{
  "actor": "alice.bsky.social"
}

Step 2: Create the Post

Tool Call: create_post

Parameters (JSON):

json
{
  "text": "Hey @alice check out #atproto at https://atproto.com",
  "facets": [
    {
      "index": {
        "byteStart": 4,
        "byteEnd": 10
      },
      "features": [
        {
          "type": "mention",
          "value": "alice.bsky.social"
        }
      ]
    },
    {
      "index": {
        "byteStart": 21,
        "byteEnd": 29
      },
      "features": [
        {
          "type": "hashtag",
          "value": "atproto"
        }
      ]
    },
    {
      "index": {
        "byteStart": 33,
        "byteEnd": 53
      },
      "features": [
        {
          "type": "link",
          "value": "https://atproto.com"
        }
      ]
    }
  ]
}

User Request:

"Generate a preview for https://example.com/article"

Tool Call: generate_link_preview

Parameters (JSON):

json
{
  "url": "https://example.com/article"
}

Response (JSON):

json
{
  "success": true,
  "message": "Link preview generated for https://example.com/article",
  "preview": {
    "uri": "https://example.com/article",
    "title": "Article Title",
    "description": "Article description text",
    "thumb": {
      "blob": {
        "type": "blob",
        "ref": "bafkreithumb...",
        "mimeType": "image/jpeg",
        "size": 45678
      }
    }
  }
}

Note: preview.thumb is only present when the page had a usable og:image. Pass the preview.thumb.blob object verbatim as embed.external.thumb in create_post.

Post with Link Preview Workflow ​

User Request:

"Create a post about the AT Protocol overview with a link preview"

Step 1: Generate Link Preview

Tool Call: generate_link_preview

Parameters (JSON):

json
{
  "url": "https://atproto.com/guides/overview"
}

Step 2: Create Post with Preview

Tool Call: create_post

Parameters (JSON):

json
{
  "text": "Great introduction to AT Protocol",
  "embed": {
    "external": {
      "uri": "https://atproto.com/guides/overview",
      "title": "AT Protocol Overview",
      "description": "Learn about the AT Protocol architecture and features",
      "thumb": {
        "type": "blob",
        "ref": "bafkreithumb...",
        "mimeType": "image/jpeg",
        "size": 45678
      }
    }
  }
}

Note: thumb is the full blob descriptor (type, ref, mimeType, size) returned as preview.thumb.blob in Step 1 — a bare ref is not enough.

Post Management ​

Delete a Post ​

User Request:

"Delete this post"

Tool Call: delete_post

Parameters (JSON):

json
{
  "uri": "at://did:plc:abc123/app.bsky.feed.post/xyz789"
}

Response (JSON):

json
{
  "success": true,
  "message": "Post deleted successfully"
}

Delete Multiple Posts Workflow ​

User Request:

"Delete these three posts"

Workflow: Call delete_post for each URI sequentially, pacing the calls per the rate-limiting note at the top of this page.

json
// Post 1
{ "uri": "at://did:plc:abc123/app.bsky.feed.post/post1" }

// Post 2
{ "uri": "at://did:plc:abc123/app.bsky.feed.post/post2" }

// Post 3
{ "uri": "at://did:plc:abc123/app.bsky.feed.post/post3" }

Delete Old Posts Workflow ​

User Request:

"Delete all my posts older than 30 days"

No match-all search

AT Protocol search has no match-all wildcard, and search_posts requires a non-empty q term — an empty query ("q": "") is rejected and will not return all of an author's posts. To enumerate your own posts you must read them from a feed (e.g. get_timeline) rather than search, then filter by date client-side before deleting.

Step 1: List Your Recent Posts

Tool Call: get_timeline

Parameters (JSON):

json
{
  "limit": 50
}

The LLM reads the returned feed, paginating with the cursor, and filters for your own posts whose createdAt is older than 30 days. (If you want posts matching a keyword instead, search_posts with a real q term plus author and until works.)

Step 2: Delete Each Post

For each old post identified above:

Tool Call: delete_post

Parameters (JSON):

json
{
  "uri": "at://did:plc:abc123/app.bsky.feed.post/oldpost"
}

Pace the deletions per the rate-limiting note at the top of this page.

Content Scheduling ​

Note: LLMs cannot directly schedule posts for future execution. Scheduling requires an external system that triggers the LLM at the scheduled time.

Scheduled Post Workflow ​

Concept: Create posts at specific times

Implementation Approach:

  1. External Scheduler (cron job, task scheduler, etc.) triggers the LLM at the desired time
  2. LLM receives trigger with the post content
  3. LLM creates the post using the create_post tool

Example Scheduled Posts:

Morning Post (triggered at 9:00 AM):

Tool Call: create_post

Parameters (JSON):

json
{
  "text": "Good morning! ☀️"
}

Evening Post (triggered at 9:00 PM):

Tool Call: create_post

Parameters (JSON):

json
{
  "text": "Good night! 🌙"
}

Alternative Approach - Queue System:

If you want the LLM to help manage a schedule:

User Request:

"I want to schedule two posts: 'Good morning! ☀️' at 9 AM and 'Good night! 🌙' at 9 PM"

LLM Response:

"I've noted your scheduled posts:

1. 'Good morning! ☀️' - scheduled for 9:00 AM
2. 'Good night! 🌙' - scheduled for 9:00 PM

Note: I cannot execute these automatically. You'll need to:
- Set up a cron job or task scheduler to trigger me at these times
- Or manually ask me to create these posts at the scheduled times
- Or use a third-party scheduling service that integrates with this MCP server

Would you like me to create either of these posts now instead?"

Content Moderation ​

Mute Users ​

User Request:

"Mute spammer.bsky.social"

Tool Call: mute_user

Parameters (JSON):

json
{
  "actor": "spammer.bsky.social"
}

Response (JSON):

json
{
  "success": true,
  "message": "User muted successfully"
}

Block Users ​

User Request:

"Block harasser.bsky.social"

Tool Call: block_user

Parameters (JSON):

json
{
  "actor": "harasser.bsky.social"
}

Response (JSON):

json
{
  "success": true,
  "blockUri": "at://did:plc:abc123/app.bsky.graph.block/block456",
  "message": "User blocked successfully"
}

Note: Store the blockUri to unblock the user later.

Report Content ​

User Request:

"Report this post for spam"

Tool Call: report_content

Parameters (JSON):

json
{
  "subject": {
    "uri": "at://did:plc:abc123/app.bsky.feed.post/xyz789",
    "cid": "bafyreiabc123..."
  },
  "reasonType": "spam",
  "reason": "Repeated promotional content"
}

Response (JSON):

json
{
  "success": true,
  "message": "Content reported successfully"
}

Reason Types:

  • "spam" - Spam or unwanted content
  • "violation" - Terms of service violation
  • "misleading" - Misleading or false information
  • "sexual" - Sexual content
  • "rude" - Rude or harassing content
  • "other" - Other reasons

Report User ​

User Request:

"Report spambot.bsky.social for being a spam account"

Tool Call: report_user

Parameters (JSON):

json
{
  "actor": "spambot.bsky.social",
  "reasonType": "spam",
  "reason": "Automated spam account"
}

Response (JSON):

json
{
  "success": true,
  "message": "User reported successfully"
}

Batch Operations ​

Batch Upload Images Workflow ​

User Request:

"Upload these 6 images"

Workflow: Upload images in batches, calling upload_image once per image and pacing the calls per the rate-limiting note at the top of this page.

json
// Batch 1
{ "filePath": "./photos/img1.jpg", "altText": "Image 1" }
{ "filePath": "./photos/img2.jpg", "altText": "Image 2" }
{ "filePath": "./photos/img3.jpg", "altText": "Image 3" }

// Batch 2
{ "filePath": "./photos/img4.jpg", "altText": "Image 4" }
{ "filePath": "./photos/img5.jpg", "altText": "Image 5" }
{ "filePath": "./photos/img6.jpg", "altText": "Image 6" }

Collect the returned image.blob descriptors to attach to posts (a single post carries at most 4 images).

Batch Create Posts Workflow ​

User Request:

"Create these three posts: 'Post 1', 'Post 2', 'Post 3'"

Workflow: Call create_post once per post, sequentially, pacing the calls per the rate-limiting note at the top of this page.

json
{ "text": "Post 1" }
{ "text": "Post 2" }
{ "text": "Post 3" }

Best Practices ​

Image Optimization ​

Note: LLMs cannot directly optimize media files. upload_image enforces a 1 MB cap and accepts only .jpg/.jpeg/.png/.gif/.webp/.avif files; upload_video enforces the video service's 100 MB cap and accepts only .mp4/.mov/.webm. Both read local file paths that must resolve within the allowed media directory (ATPROTO_MEDIA_DIR, defaults to the working directory). Optimization should be done before the file is handed to these tools.

When a user provides an oversized file, the LLM can suggest resizing or compressing it (e.g. JPEG/WebP for images, H.264 MP4 for video) to fit within these limits before uploading.

Error Recovery Workflow ​

Concept: Retry failed uploads with exponential backoff

First Attempt:

Tool Call: upload_image

Parameters (JSON):

json
{
  "filePath": "./photos/photo.jpg",
  "altText": "Photo description"
}

If Error Occurs:

Wait 1 second, then retry

Second Attempt:

Same parameters as first attempt

If Error Occurs Again:

Wait 2 seconds, then retry

Third Attempt:

Same parameters as first attempt

If Error Occurs Third Time:

Inform the user:

"I've tried uploading the image 3 times but encountered errors. This might be due to:
- Network connectivity issues
- Image file corruption
- Server-side problems

Please try again later or with a different image."

Best Practice: Use exponential backoff (1s, 2s, 4s) between retries.

See Also ​