create_post
Create a new post on AT Protocol with support for text, replies, images, a video (from upload_video), external links, quote posts, richtext facets, and language tags. This is the single rich post-creation tool (it replaces the former create_rich_text_post): mentions, links, and #hashtags in the text are auto-detected into richtext facets unless you supply facets explicitly.
Authentication
Required: Yes (Private tool)
This tool requires authentication using an app password (OAuth is not yet implemented).
Parameters
text (required)
- Type:
string - Constraints:
- Minimum length: 1 character
- Maximum length: 300 graphemes / 3000 UTF-8 bytes (emoji count as one grapheme)
- Description: The text content of the post
reply (optional)
- Type:
object - Description: Reply information if this post is a reply to another post
- Properties:
root(required):string- AT-URI of the root post in the threadparent(required):string- AT-URI of the immediate parent post
Both
rootandparentmust referenceapp.bsky.feed.postrecords. An AT-URI naming any other record type (a like, a follow, …) is rejected with a validation error, since it would produce a structurally invalid reply.
embed (optional)
- Type:
object - Description: Optional media embed: images OR an external link card OR a video (at most one). Mutually exclusive with
quote— a post may carry images, an external link, a video, OR a quote (record), never a combination. - Properties:
images(optional): Array of image objects (max 4)alt(required):string- Alt text for accessibility (max 1000 characters)image(required):object- Pre-uploaded blob descriptor: pass theimage.blobobject returned by upload_image verbatim ({ type: 'blob', ref, mimeType, size };refmay be the flat CID string or the lexicon{ "$link": "<cid>" }form). This tool does not accept raw image data — upload first, then reference the blob.
external(optional): External link objecturi(required):string- Valid URLtitle(required):string- Link title (max 300 characters)description(required):string- Link description (max 1000 characters)thumb(optional):object- Thumbnail for the link card as a pre-uploaded blob descriptor: pass thepreview.thumb.blobobject from generate_link_preview (or theimage.blobfrom upload_image) verbatim. Omit for a card without a thumbnail.
video(optional): A video embed (app.bsky.embed.video)video(required):object- Pre-uploaded processed video blob descriptor: pass thevideo.blobobject returned by upload_video verbatim ({ type: 'blob', ref, mimeType, size };refmay be the flat CID string or the lexicon{ "$link": "<cid>" }form). The video must already have been uploaded and transcoded by the video service — this tool does not accept raw video data.captions(optional): Array of caption tracks (max 20)lang(required):string- BCP-47 language code for the caption track (at least 2 characters, e.g."en","fr","pt-BR")file(required):object- Pre-uploaded WebVTT caption blob descriptor: pass avideo.captions[].fileobject from upload_video verbatim
alt(optional):string- Alt text describing the video for accessibility (max 1000 characters)aspectRatio(optional):object- Aspect ratio hint clients use to reserve layout space before the video loadswidth(required):number- Width component (positive integer)height(required):number- Height component (positive integer)
facets (optional)
- Type:
object[] - Description: Explicit richtext facets (byte-range annotations). Omit to let the server auto-detect mentions, links, and #hashtags from the text. Supplying
facetsdisables auto-detection (applying both would double-annotate the text). - Properties (per facet):
index(required):object- UTF-8 byte range of the annotated spanbyteStart(required):number- Start byte offset (UTF-8), inclusivebyteEnd(required):number- End byte offset (UTF-8), exclusive
features(required): Array of feature objects applied to the spantype(required):"mention"|"link"|"hashtag"value(required):string- For a mention, a handle or DID; for a link, the URL; for a hashtag, the tag without the leading#
Caller-supplied facets are validated: each byte range must satisfy
byteStart < byteEnd <= textByteLength(the text's UTF-8 byte length), and mention handles are resolved to DIDs.
quote (optional)
- Type:
object - Description: Quote another post (record embed). Mutually exclusive with
embed.images,embed.external, andembed.video. - Properties:
uri(required):string- AT-URI of the post to quotecid(required):string- CID (content hash) of the quoted post
langs (optional)
- Type:
string[] - Description: Array of BCP-47 language tags (e.g.
"en","en-US","pt-BR") - Example:
["en", "es"]
replyControls (optional)
- Type:
object - Description: Who can reply to the post. After the post is created, an
app.bsky.feed.threadgaterecord is written with the same rkey as the post (a lexicon requirement). Enabled options combine into up to 5 allow rules. Providing the object with no rules enabled writes an empty allow list, which means nobody can reply; omittingreplyControlsentirely writes no record and leaves replies open to everyone. - Properties:
allowMentioned(optional):boolean- Allow replies from accounts @-mentioned in the post text (mentionRule)allowFollowing(optional):boolean- Allow replies from accounts you follow (followingRule)allowFollowers(optional):boolean- Allow replies from accounts that follow you (followerRule)allowListUris(optional):string[]- Allow replies from members of these lists (listRule, max 5 rules total). Each entry must be the AT-URI of anapp.bsky.graph.listrecord (at://did/app.bsky.graph.list/rkey); anything else is rejected with a validation error before the post is created.
quoteControls (optional)
- Type:
object - Description: Quote (embed) policy for the post.
- Properties:
allowQuotes(required):boolean- Setfalseto disable quoting: after the post is created, anapp.bsky.feed.postgaterecord (same rkey as the post) is written with adisableRule.trueis the network default — quoting stays enabled and no postgate record is written.
Partial-failure contract: the gate records are written after the post exists. If a gate write fails at that point, the call still returns
success: true(the post is live) withgateApplied: falseand awarningexplaining which gate failed and how to retry — it does not fail the whole call.
Response
Returns an object with the following properties:
{
uri: string; // AT Protocol URI of the created post
cid: string; // Content identifier (CID) of the post
success: boolean; // Whether the operation succeeded
message: string; // Success message
gateApplied?: boolean; // Only when replyControls/quoteControls were given:
// true when every requested gate record was written; false when the post
// was created but a gate write failed (post is live without the controls)
warning?: string; // Only when gateApplied is false: which gate failed and
// how to retry
}Examples
Basic Text Post
{
"text": "Hello from the AT Protocol MCP Server! 👋"
}Response:
{
"uri": "at://did:plc:abc123/app.bsky.feed.post/xyz789",
"cid": "bafyreiabc123...",
"success": true,
"message": "Post created successfully"
}Post with Language Tags
{
"text": "Bonjour le monde! Hello world!",
"langs": ["fr", "en"]
}Reply to a Post
{
"text": "Great point! I totally agree.",
"reply": {
"root": "at://did:plc:abc123/app.bsky.feed.post/root123",
"parent": "at://did:plc:abc123/app.bsky.feed.post/parent456"
}
}Post with Images
Images must be uploaded first with upload_image; pass each returned image.blob object verbatim as embed.images[].image:
{
"text": "Check out these amazing photos!",
"embed": {
"images": [
{
"alt": "A beautiful sunset over the ocean",
"image": {
"type": "blob",
"ref": "bafkreiabc123...",
"mimeType": "image/jpeg",
"size": 245678
}
},
{
"alt": "Mountains in the distance",
"image": {
"type": "blob",
"ref": "bafkreidef456...",
"mimeType": "image/png",
"size": 198432
}
}
]
}
}Post with Video
Upload the video first with upload_video — it returns the processed blob descriptor once the video service finishes transcoding. Suppose it returned:
{
"success": true,
"message": "Video uploaded and processed successfully from ./videos/tutorial.mp4",
"video": {
"blob": {
"type": "blob",
"ref": "bafkreivideo123...",
"mimeType": "video/mp4",
"size": 4853210
},
"alt": "Tutorial on using AT Protocol",
"jobId": "rmpdzv4uoctlginpv3oddi6w",
"captions": [
{
"lang": "en",
"file": {
"type": "blob",
"ref": "bafkreicaption789...",
"mimeType": "text/vtt",
"size": 1843
}
}
]
}
}Then pass video.blob verbatim as embed.video.video and each video.captions[] entry verbatim under embed.video.captions:
{
"text": "New tutorial is up! 🎬",
"embed": {
"video": {
"video": {
"type": "blob",
"ref": "bafkreivideo123...",
"mimeType": "video/mp4",
"size": 4853210
},
"alt": "Tutorial on using AT Protocol",
"aspectRatio": { "width": 16, "height": 9 },
"captions": [
{
"lang": "en",
"file": {
"type": "blob",
"ref": "bafkreicaption789...",
"mimeType": "text/vtt",
"size": 1843
}
}
]
}
}
}Post with External Link
{
"text": "Interesting article about AT Protocol",
"embed": {
"external": {
"uri": "https://example.com/article",
"title": "Understanding AT Protocol",
"description": "A comprehensive guide to the AT Protocol architecture and features"
}
}
}Post with External Link and Thumbnail
Call generate_link_preview first, then reuse its preview fields — including the preview.thumb.blob descriptor — so the link card carries a thumbnail:
{
"text": "Interesting article about AT Protocol",
"embed": {
"external": {
"uri": "https://example.com/article",
"title": "Understanding AT Protocol",
"description": "A comprehensive guide to the AT Protocol architecture and features",
"thumb": {
"type": "blob",
"ref": "bafkreighi789...",
"mimeType": "image/jpeg",
"size": 45678
}
}
}
}Quote Post
{
"text": "This is a great take 👇",
"quote": {
"uri": "at://did:plc:abc123/app.bsky.feed.post/xyz789",
"cid": "bafyreiabc123..."
}
}Post with Reply and Quote Controls
Only people the author follows or members of a list may reply, and quoting is disabled:
{
"text": "Sharing this with a smaller circle.",
"replyControls": {
"allowFollowing": true,
"allowListUris": ["at://did:plc:abc123/app.bsky.graph.list/trusted"]
},
"quoteControls": { "allowQuotes": false }
}Response:
{
"uri": "at://did:plc:abc123/app.bsky.feed.post/xyz789",
"cid": "bafyreiabc123...",
"success": true,
"message": "Post created successfully",
"gateApplied": true
}To lock replies entirely (nobody can reply), pass replyControls with no rules enabled:
{
"text": "Announcement only — replies are closed.",
"replyControls": {}
}If the post is created but a gate write then fails, the call still succeeds:
{
"uri": "at://did:plc:abc123/app.bsky.feed.post/xyz789",
"cid": "bafyreiabc123...",
"success": true,
"message": "Post created successfully",
"gateApplied": false,
"warning": "Reply controls could not be applied (the app.bsky.feed.threadgate write failed: ...). The post itself was created and replies are currently OPEN; retry by writing a threadgate record with rkey \"xyz789\"."
}Post with Explicit Richtext Facets
Link the text "AT Protocol" (bytes 11-22) to a URL instead of relying on auto-detection:
{
"text": "Read about AT Protocol today!",
"facets": [
{
"index": { "byteStart": 11, "byteEnd": 22 },
"features": [{ "type": "link", "value": "https://atproto.com" }]
}
]
}Error Handling
Common Errors
Authentication Required
{
"error": "Authentication required",
"code": "AUTHENTICATION_FAILED"
}Text Too Long
The limit is 300 graphemes and 3000 UTF-8 bytes, counted on graphemes so emoji-heavy posts are not falsely rejected:
{
"error": "Post text is 312 graphemes; the maximum is 300.",
"code": "VALIDATION_ERROR"
}Too Many Images
{
"error": "Cannot attach more than 4 images",
"code": "VALIDATION_ERROR"
}Invalid Reply URI
{
"error": "Invalid AT Protocol URI format",
"code": "VALIDATION_ERROR"
}Invalid Reply-Control List URI
replyControls.allowListUris entries must reference app.bsky.graph.list records; this is validated before the post is created, so nothing is published:
{
"error": "replyControls.allowListUris entries must be AT-URIs of app.bsky.graph.list records (at://did/app.bsky.graph.list/rkey); got \"at://did:plc:abc123/app.bsky.feed.post/xyz\".",
"code": "VALIDATION_ERROR"
}Multiple Embeds Combined
A post can include only one embed: images, an external link, a video, or a quote (record) — not a combination.
{
"error": "A post can include only one embed: images, an external link, a video, or a quote (record) — not a combination. Provide only one.",
"code": "VALIDATION_ERROR"
}Rate Limit Exceeded (this server's per-tool limit)
When this server's own per-tool limit is exceeded, the tool returns an MCP error with code InternalError and no machine-readable retryAfter value:
{
"error": "Rate limit exceeded for tool \"create_post\". Please slow down and retry shortly.",
"code": "InternalError"
}Rate Limit Exceeded (upstream Bluesky API)
Separately, if the upstream Bluesky API returns an HTTP 429, the tool surfaces a RATE_LIMIT_EXCEEDED error that includes a retryAfter value (seconds to wait) derived from the response's retry-after header:
{
"error": "Rate limit exceeded. Please try again later.",
"code": "RATE_LIMIT_EXCEEDED",
"retryAfter": 60
}Best Practices
Text Content
- Keep posts concise and within 300 graphemes (3000 UTF-8 bytes)
- Use proper formatting and line breaks for readability
- Include relevant hashtags for discoverability
Images
- Upload images first with
upload_image, then pass each returnedimage.blobdescriptor asembed.images[].image - Always provide descriptive alt text for accessibility
- Maximum 4 images per post (Bluesky platform limit)
Video
- Upload the video first with
upload_video(it waits for the video service to finish transcoding), then pass the returnedvideo.blobdescriptor asembed.video.video - Pass any
video.captions[].filedescriptors fromupload_videoverbatim asembed.video.captions[].file(max 20 caption tracks) - Provide
alttext, and anaspectRatiohint when you know the dimensions - One video per post; a video cannot be combined with images, an external link, or a quote
Replies
- Always include both
rootandparentURIs when replying - The
rootshould be the first post in the thread - The
parentshould be the immediate post you're replying to
Language Tags
- Use BCP-47 language tags (e.g.
en,en-US,pt-BR) - Include all languages present in the post text
- Helps with content filtering and discovery
External Links
- Provide accurate and descriptive titles
- Write clear descriptions that summarize the linked content
- Ensure URLs are valid and accessible
Reply and Quote Controls
- The gate records are keyed by the post's own rkey — they only exist after the post does, so always check
gateAppliedin the response when you supplyreplyControlsorquoteControls replyControls: {}(no rules enabled) means nobody can reply; omit the field to leave replies openquoteControls: { "allowQuotes": true }is a no-op (the network default); onlyfalsewrites a postgate record
Rate Limiting
This server applies a per-tool rate limit of 100 requests per minute. When this server's own limit is exceeded, the tool returns an MCP error with code InternalError and a message like Rate limit exceeded for tool "create_post". Please slow down and retry shortly. (no machine-readable retryAfter value).
Separately, if the upstream Bluesky API returns an HTTP 429, the tool surfaces a RATE_LIMIT_EXCEEDED error that includes a retryAfter value (seconds to wait) derived from the response's retry-after header.
Related Tools
- reply_to_post - Simplified tool specifically for replies
- create_thread - Publish a multi-post chain in one call
- upload_image - Upload images separately before posting
- upload_video - Upload and process a video before posting
- generate_link_preview - Generate link preview data