MCP Server

bluesky-mcp-server

io.github.cyanheads/bluesky-mcp-server
Social Media Public & reachable MCP 2025-11-25

What this MCP does

Searches and retrieves public Bluesky posts, profiles, feeds, threads, social graphs, and trending topics.

bsky_get_author_feed
Get Bluesky Author Feed
Get a Bluesky user's recent feed ordered newest-first. Filter by post type: "posts_with_replies" (everything), "posts_no_replies" (excludes replies), "posts_and_author_threads" (posts the author started), "posts_with_media" (the actor's own posts with images or video — no link cards), or "posts_with_video" (the actor's own video posts). The first three include reposts, so items authored by other accounts appear alongside the actor's own writing — a "repostedBy" field marks those, and the "author" field always names who actually wrote the post; the two media filters return no reposts. Set include_pins to also get the post pinned to the profile, marked "pinned", first on the first page — in addition to "limit", and whether or not it matches the filter. Returns posts with full text, engagement counts, embeds, and AT-URIs for drilling into threads via bsky_get_post_thread. Because "limit" counts reposts too, a page from an account that reposts heavily holds far fewer of that account's own posts than the limit suggests; the enrichment fields report the split, so read "originalPosts" rather than the limit when you want the actor's own writing. Supports cursor pagination.
Read only Open world Idempotent
Input schema
{'type': 'object', '$schema': 'https://json-schema.org/draft/2020-12/schema', 'required': ['actor'], 'properties': {'actor': {'type': 'string', 'pattern': '^(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]|@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|https:\\/\\/bsky\\.app\\/profile\\/(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-])\\/?(?:[?#].*)?)$', 'maxLength': 2048, 'minLength': 1, 'description': 'Handle (e.g. "alice.bsky.social") or DID of the author whose feed to fetch. A leading "@" and the account\'s bsky.app page ("https://bsky.app/profile/alice.bsky.social") are accepted and read as the handle or DID they carry. A bare name without a dot is not a handle â\x80\x94 use bsky_search_actors to resolve one.'}, 'limit': {'type': 'integer', 'default': 25, 'maximum': 100, 'minimum': 1, 'description': 'Maximum number of posts to return (1â\x80\x93100). Default 25. A page that would pass the 48,000-byte response budget comes back with fewer posts and "budgetCapped: true"; its cursor continues from the first post it left out.'}, 'cursor': {'type': 'string', 'maxLength': 2048, 'description': 'Opaque pagination cursor from a previous response for the same actor, passed back unchanged. Omit for the first page.'}, 'filter': {'enum': ['posts_with_replies', 'posts_no_replies', 'posts_with_media', 'posts_and_author_threads', 'posts_with_video'], 'type': 'string', 'default': 'posts_no_replies', 'description': 'Filter for post types: "posts_no_replies" excludes replies, "posts_with_replies" for everything, "posts_and_author_threads" for threads the author started â\x80\x94 all three include reposts, so check "repostedBy" on each item. "posts_with_media" returns the actor\'s own posts with images or video, not link cards, and "posts_with_video" their video posts; neither includes reposts.'}, 'include_pins': {'type': 'boolean', 'default': False, 'description': 'Also return the post pinned to the actor\'s profile, marked "pinned: true", first on the first page. It arrives in addition to "limit", whether or not it matches "filter", and is not repeated on later pages. Default false.'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'anyOf': [{'not': {'required': ['error']}, 'required': ['posts', 'totalReturned']}, {'required': ['error']}], '$schema': 'https://json-schema.org/draft/2020-12/schema', 'properties': {'cap': {'type': 'number', 'description': 'The limit applied to this page.'}, 'error': {'type': 'object', 'required': ['code', 'message'], 'properties': {'code': {'type': 'integer', 'maximum': 9007199254740991, 'minimum': -9007199254740991, 'description': 'JSON-RPC error code for this failure.'}, 'data': {'type': 'object', 'properties': {'reason': {'type': 'string', 'examples': ['actor_not_found', 'invalid_cursor'], 'description': 'Machine-readable failure mode. Declared by this tool: `actor_not_found`: The actor handle or DID does not resolve to an existing account. `invalid_cursor`: Bluesky could not continue from the cursor the request carried â\x80\x94 it answers a cursor it cannot decode with HTTP 500. Other values are possible when a failure originates below the handler.'}, 'recovery': {'type': 'object', 'required': ['hint'], 'properties': {'hint': {'type': 'string'}}, 'description': 'Actionable next step for the caller.', 'additionalProperties': {}}, 'retryable': {'type': 'boolean', 'description': 'Whether retrying may succeed.'}}, 'additionalProperties': {}}, 'message': {'type': 'string', 'description': 'Human-readable description of what went wrong.'}}, 'description': 'Present when the call failed. Absent on success.', 'additionalProperties': {}}, 'posts': {'type': 'array', 'items': {'type': 'object', 'required': ['uri', 'cid', 'text', 'author'], 'properties': {'cid': {'type': 'string', 'description': 'Content Identifier (CID) of the post record.'}, 'uri': {'type': 'string', 'description': 'AT-URI of the post, e.g. "at://did:plc:xxx/app.bsky.feed.post/yyy". Use with bsky_get_post_thread.'}, 'text': {'type': 'string', 'description': 'Full text content of the post.'}, 'embed': {'type': 'object', 'properties': {}, 'description': 'Media or link embed attached to this post. type: "images" | "external" | "record" | "video" | "unknown". images: array of { url, alt } â\x80\x94 also carries app.bsky.embed.gallery embeds. external: { uri, title, description }. record: { uri, cid, text?, authorHandle?, embeds?, media?, omittedEmbeds?, recordKind? } â\x80\x94 embeds is the quoted post\'s own attachments, so a quote of an image post carries those images here; media is the image/video/link attached alongside the quote by the post doing the quoting, on a recordWithMedia embed. Both are embeds of these same shapes. Bluesky fills embeds for the post being quoted and no deeper, so a quote nested inside another quote ordinarily carries none; omittedEmbeds counts any it did carry that were past the nesting this server follows, so an unattached quote and one whose attachments are missing are never the same value. Fetch the quote uri as its own post to read them. recordKind is absent for an ordinary quoted post and otherwise names what stood in for one: "notFound" | "blocked" | "detached" (the quote exists but cannot be read) or "generator" | "list" | "starterPack" | "labeler" | "unknown" (the quoted record is not a post). A "generator" quote is a feed: pass its uri to bsky_get_feed to read it. When recordKind is set, text and authorHandle are absent because that variant does not carry them â\x80\x94 do not read the quote as an empty post. video: { playlist?, thumbnail?, presentation? }. unknown: { raw } â\x80\x94 raw is the upstream $type this server has no mapping for.', 'additionalProperties': {}}, 'author': {'type': 'object', 'required': ['did', 'handle'], 'properties': {'did': {'type': 'string', 'description': 'Permanent DID of the author, e.g. "did:plc:z72i7hdynmk6r22z27h6tvur".'}, 'avatar': {'type': 'string', 'description': 'URL of the author avatar image.'}, 'handle': {'type': 'string', 'description': 'Human-readable handle of the author, e.g. "alice.bsky.social".'}, 'displayName': {'type': 'string', 'description': 'Display name set by the author.'}, 'verification': {'type': 'object', 'required': ['verifiedStatus', 'trustedVerifierStatus'], 'properties': {'verifiedStatus': {'type': 'string', 'description': 'Whether a trusted verifier verified the author: "valid", "invalid" (verified once, no longer holds), or "none". Passed through as Bluesky sends it, so another value may appear.'}, 'trustedVerifierStatus': {'type': 'string', 'description': 'Whether the author is itself a trusted verifier â\x80\x94 same values as verifiedStatus.'}}, 'description': 'Bluesky verification of the author â\x80\x94 what tells a verified account from a look-alike handle. Absent when Bluesky sent none. Who issued it is on bsky_get_profile.', 'additionalProperties': False}}, 'description': 'Author of this post.', 'additionalProperties': False}, 'labels': {'type': 'array', 'items': {'type': 'object', 'required': ['val'], 'properties': {'cts': {'type': 'string', 'description': 'ISO 8601 timestamp when the label was applied.'}, 'src': {'type': 'string', 'description': 'DID of the labeler that applied this label. Equal to the post author DID when the account labelled its own post, and a labeler service DID otherwise.'}, 'val': {'type': 'string', 'description': 'Label value (content warning or moderation tag, e.g. "porn", "spam").'}}, 'description': 'A moderation label applied by the AppView or a labeler service.', 'additionalProperties': False}, 'description': 'Moderation labels on this post.'}, 'pinned': {'type': 'boolean', 'description': 'True on the post the actor pinned to their profile, returned when include_pins is set. A pin is placement, not recency â\x80\x94 the post is often older than the items below it. Absent on every other item.'}, 'createdAt': {'type': 'string', 'description': 'ISO 8601 timestamp when this post was created.'}, 'indexedAt': {'type': 'string', 'description': 'ISO 8601 timestamp when this post was indexed.'}, 'likeCount': {'type': 'number', 'description': 'Number of likes.'}, 'quoteCount': {'type': 'number', 'description': 'Number of quote posts Bluesky counts â\x80\x94 read them with bsky_get_post_quotes. An upper bound on what that returns, since the counter keeps quotes that have left the index.'}, 'replyCount': {'type': 'number', 'description': 'Number of replies to this post.'}, 'replyToUri': {'type': 'string', 'description': 'AT-URI of the post this is a reply to, if applicable.'}, 'repostedAt': {'type': 'string', 'description': 'ISO 8601 timestamp of the repost. Present only on reposted items.'}, 'repostedBy': {'type': 'object', 'required': ['did', 'handle'], 'properties': {'did': {'type': 'string', 'description': 'Permanent DID of the account that reposted.'}, 'handle': {'type': 'string', 'description': 'Handle of the account that reposted.'}, 'displayName': {'type': 'string', 'description': 'Display name of the account that reposted.'}}, 'description': 'Present only when this item is a repost rather than the requested actor writing. The post itself â\x80\x94 text, author, engagement counts â\x80\x94 belongs to the author field, not to this account.', 'additionalProperties': False}, 'repostCount': {'type': 'number', 'description': 'Number of reposts.'}, 'replyRootUri': {'type': 'string', 'description': 'AT-URI of the post this conversation started from, if this is a reply. Pass to bsky_get_post_thread to read the whole conversation rather than one branch.'}}, 'description': "A single item from the author feed â\x80\x94 the actor's own post, or a post they reposted.", 'additionalProperties': False}, 'description': 'Feed items, newest-first â\x80\x94 the actor\'s own posts and the posts they reposted. Items carrying "repostedBy" were written by the account named in "author", not by the requested actor.'}, 'shown': {'type': 'number', 'description': 'Number of posts returned on this page.'}, 'cursor': {'type': 'string', 'description': 'Opaque cursor for the next page. Absent on the last page.'}, 'notice': {'type': 'string', 'description': 'Guidance when the result set is empty or constrained.'}, 'reposts': {'type': 'number', 'description': 'How many items on this page are posts the requested actor reposted rather than wrote. Present only when there is at least one; these items carry "repostedBy".'}, 'truncated': {'type': 'boolean', 'description': 'True when more posts exist beyond this page (a cursor was returned).'}, 'budgetCapped': {'type': 'boolean', 'description': 'True when a page of the requested limit would have passed this server\'s 48,000-byte response budget, so Bluesky was asked again for fewer posts and that page was returned whole. The cursor comes from that same response, so paging on from it skips nothing. Independent of "truncated", which still means only that a cursor was returned.'}, 'originalPosts': {'type': 'number', 'description': 'How many items on this page the requested actor wrote, a pinned post included. Present whenever the page carries at least one repost â\x80\x94 the number a caller asking for the actor\'s own writing is after, since under the filters that include reposts "limit" counts them too.'}, 'totalReturned': {'type': 'number', 'description': 'Number of posts in this response page.'}}, 'additionalProperties': False}
bsky_get_feed
Get Bluesky Feed
Read the posts a Bluesky feed generator serves, in the order the feed ranks them. Accepts the feed's AT-URI (at://<handle-or-did>/app.bsky.feed.generator/<rkey>) or its bsky.app page (https://bsky.app/profile/<handle-or-did>/feed/<rkey>). Feeds come from the "feedUri" of each bsky_get_trending topic — the way to read what a trend is about — from a quoted feed in a post (an embed with recordKind "generator"), or from a shared link; Discover is at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot. Returns posts with full text, engagement counts, embeds, and AT-URIs for drilling into threads via bsky_get_post_thread. A post the feed pinned to its top carries "pinned: true"; a repost carries "repostedBy". Personalized feeds, which Bluesky serves only to a signed-in account, cannot be read here. Supports cursor pagination.
Read only Open world Idempotent
Input schema
{'type': 'object', '$schema': 'https://json-schema.org/draft/2020-12/schema', 'required': ['feed'], 'properties': {'feed': {'type': 'string', 'pattern': '^(?:at:\\/\\/(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-])\\/app\\.bsky\\.feed\\.generator\\/[a-zA-Z0-9._~:-]{1,512}|https:\\/\\/bsky\\.app\\/profile\\/(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-])\\/feed\\/[a-zA-Z0-9._~:-]{1,512}\\/?(?:[?#].*)?)$', 'maxLength': 2048, 'description': 'The feed to read â\x80\x94 its AT-URI, e.g. "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot", or its bsky.app URL, e.g. "https://bsky.app/profile/bsky.app/feed/whats-hot"; a trailing "/", "?â\x80¦", or "#â\x80¦" on the URL is ignored. The owner may be a handle or a DID; a handle costs one extra lookup. A trend\'s "feedUri" works as-is.'}, 'limit': {'type': 'integer', 'default': 25, 'maximum': 100, 'minimum': 1, 'description': 'Maximum number of posts to return (1â\x80\x93100). Default 25. A feed may return fewer than the limit on a page that still has more after it â\x80\x94 follow the cursor, not the count. A page that would pass the 48,000-byte response budget is asked for again with fewer posts and carries "budgetCapped: true"; its cursor continues after the posts it holds.'}, 'cursor': {'type': 'string', 'maxLength': 2048, 'description': 'Opaque pagination cursor from a previous response of the same feed. Omit for the first page.'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'anyOf': [{'not': {'required': ['error']}, 'required': ['posts', 'totalReturned']}, {'required': ['error']}], '$schema': 'https://json-schema.org/draft/2020-12/schema', 'properties': {'cap': {'type': 'number', 'description': 'The limit applied to this page.'}, 'error': {'type': 'object', 'required': ['code', 'message'], 'properties': {'code': {'type': 'integer', 'maximum': 9007199254740991, 'minimum': -9007199254740991, 'description': 'JSON-RPC error code for this failure.'}, 'data': {'type': 'object', 'properties': {'reason': {'type': 'string', 'examples': ['feed_not_found', 'feed_unavailable', 'feed_requires_login'], 'description': 'Machine-readable failure mode. Declared by this tool: `feed_not_found`: No feed generator exists at that address, or the handle in it does not resolve to an account. `feed_unavailable`: The feed exists but the service that generates it did not answer â\x80\x94 offline, misconfigured, or erroring. `feed_requires_login`: The feed is personalized and Bluesky serves it only to a signed-in account. Other values are possible when a failure originates below the handler.'}, 'recovery': {'type': 'object', 'required': ['hint'], 'properties': {'hint': {'type': 'string'}}, 'description': 'Actionable next step for the caller.', 'additionalProperties': {}}, 'retryable': {'type': 'boolean', 'description': 'Whether retrying may succeed.'}}, 'additionalProperties': {}}, 'message': {'type': 'string', 'description': 'Human-readable description of what went wrong.'}}, 'description': 'Present when the call failed. Absent on success.', 'additionalProperties': {}}, 'posts': {'type': 'array', 'items': {'type': 'object', 'required': ['uri', 'cid', 'text', 'author'], 'properties': {'cid': {'type': 'string', 'description': 'Content Identifier (CID) of the post record.'}, 'uri': {'type': 'string', 'description': 'AT-URI of the post, e.g. "at://did:plc:xxx/app.bsky.feed.post/yyy". Use with bsky_get_post_thread.'}, 'text': {'type': 'string', 'description': 'Full text content of the post.'}, 'embed': {'type': 'object', 'properties': {}, 'description': 'Media or link embed attached to this post. type: "images" | "external" | "record" | "video" | "unknown". images: array of { url, alt } â\x80\x94 also carries app.bsky.embed.gallery embeds. external: { uri, title, description }. record: { uri, cid, text?, authorHandle?, embeds?, media?, omittedEmbeds?, recordKind? } â\x80\x94 embeds is the quoted post\'s own attachments; media is the image/video/link attached alongside the quote by the post doing the quoting, on a recordWithMedia embed. Both are embeds of these same shapes. omittedEmbeds counts attachments past the nesting this server follows â\x80\x94 fetch the quote uri as its own post to read them. recordKind is absent for an ordinary quoted post and otherwise names what stood in for one: "notFound" | "blocked" | "detached" (the quote exists but cannot be read) or "generator" | "list" | "starterPack" | "labeler" | "unknown" (the quoted record is not a post). A "generator" quote is a feed: pass its uri to bsky_get_feed to read it. When recordKind is set, text and authorHandle are absent because that variant does not carry them. video: { playlist?, thumbnail?, presentation? }. unknown: { raw } â\x80\x94 raw is the upstream $type this server has no mapping for.', 'additionalProperties': {}}, 'author': {'type': 'object', 'required': ['did', 'handle'], 'properties': {'did': {'type': 'string', 'description': 'Permanent DID of the author, e.g. "did:plc:z72i7hdynmk6r22z27h6tvur".'}, 'avatar': {'type': 'string', 'description': 'URL of the author avatar image.'}, 'handle': {'type': 'string', 'description': 'Human-readable handle of the author, e.g. "alice.bsky.social".'}, 'displayName': {'type': 'string', 'description': 'Display name set by the author.'}, 'verification': {'type': 'object', 'required': ['verifiedStatus', 'trustedVerifierStatus'], 'properties': {'verifiedStatus': {'type': 'string', 'description': 'Whether a trusted verifier verified the author: "valid", "invalid" (verified once, no longer holds), or "none". Passed through as Bluesky sends it, so another value may appear.'}, 'trustedVerifierStatus': {'type': 'string', 'description': 'Whether the author is itself a trusted verifier â\x80\x94 same values as verifiedStatus.'}}, 'description': 'Bluesky verification of the author â\x80\x94 what tells a verified account from a look-alike handle. Absent when Bluesky sent none. Who issued it is on bsky_get_profile.', 'additionalProperties': False}}, 'description': 'Author of this post.', 'additionalProperties': False}, 'labels': {'type': 'array', 'items': {'type': 'object', 'required': ['val'], 'properties': {'cts': {'type': 'string', 'description': 'ISO 8601 timestamp when the label was applied.'}, 'src': {'type': 'string', 'description': 'DID of the labeler that applied this label. Equal to the post author DID when the account labelled its own post, and a labeler service DID otherwise.'}, 'val': {'type': 'string', 'description': 'Label value (content warning or moderation tag, e.g. "porn", "spam").'}}, 'description': 'A moderation label applied by the AppView or a labeler service.', 'additionalProperties': False}, 'description': 'Moderation labels on this post.'}, 'pinned': {'type': 'boolean', 'description': 'True when the feed pinned this post to its top. A pin is placement, not recency â\x80\x94 the post is often older than the items below it. Absent on every other item.'}, 'createdAt': {'type': 'string', 'description': 'ISO 8601 timestamp when this post was created.'}, 'indexedAt': {'type': 'string', 'description': 'ISO 8601 timestamp when this post was indexed.'}, 'likeCount': {'type': 'number', 'description': 'Number of likes.'}, 'quoteCount': {'type': 'number', 'description': 'Number of quote posts Bluesky counts â\x80\x94 read them with bsky_get_post_quotes. An upper bound on what that returns, since the counter keeps quotes that have left the index.'}, 'replyCount': {'type': 'number', 'description': 'Number of replies to this post.'}, 'replyToUri': {'type': 'string', 'description': 'AT-URI of the post this is a reply to, if applicable.'}, 'repostedAt': {'type': 'string', 'description': 'ISO 8601 timestamp of the repost. Present only on reposted items.'}, 'repostedBy': {'type': 'object', 'required': ['did', 'handle'], 'properties': {'did': {'type': 'string', 'description': 'Permanent DID of the account that reposted.'}, 'handle': {'type': 'string', 'description': 'Handle of the account that reposted.'}, 'displayName': {'type': 'string', 'description': 'Display name of the account that reposted.'}}, 'description': 'Present only when the feed served this item as a repost. The post itself â\x80\x94 text, author, engagement counts â\x80\x94 belongs to the author field, not to this account.', 'additionalProperties': False}, 'repostCount': {'type': 'number', 'description': 'Number of reposts.'}, 'replyRootUri': {'type': 'string', 'description': 'AT-URI of the post this conversation started from, if this is a reply. Pass to bsky_get_post_thread to read the whole conversation rather than one branch.'}}, 'description': 'A single post the feed served.', 'additionalProperties': False}, 'description': 'Posts the feed served, in the order it ranked them.'}, 'shown': {'type': 'number', 'description': 'Number of posts returned on this page.'}, 'cursor': {'type': 'string', 'description': 'Opaque cursor for the next page. Absent when the feed has nothing further.'}, 'notice': {'type': 'string', 'description': 'Guidance when the feed returned nothing, or was cut.'}, 'truncated': {'type': 'boolean', 'description': 'True when the feed has more posts after this page (a cursor was returned).'}, 'budgetCapped': {'type': 'boolean', 'description': 'True when a page of the requested limit would have passed this server\'s 48,000-byte response budget, so the feed was asked again for fewer posts and that page was returned whole. The cursor comes from that same response, so paging on from it continues where these posts end. Independent of "truncated", which still means only that a cursor was returned.'}, 'totalReturned': {'type': 'number', 'description': 'Number of posts in this response page.'}}, 'additionalProperties': False}
bsky_get_follows
Get Bluesky Social Graph
Fetch the social graph edges for a Bluesky account — who follows them, or who they follow. Returns paginated actor profiles (handle, DID, displayName, bio, pronouns when set, and Bluesky verification status) plus a summary of the subject account. Follower, following, and post counts and the website are not on this view, for the listed accounts or the subject — bsky_get_profile returns them for one account. Accounts with large social graphs return only the first page; use cursor pagination to walk through the full list, or sort "top" to put the accounts Bluesky ranks most prominent first.
Read only Open world Idempotent
Input schema
{'type': 'object', '$schema': 'https://json-schema.org/draft/2020-12/schema', 'required': ['actor', 'direction'], 'properties': {'sort': {'enum': ['latest', 'top'], 'type': 'string', 'description': 'Order of the list. "latest" (Bluesky\'s default, and what omitting this gives) puts the most recent follows first; "top" is Bluesky\'s own ranking, which surfaces prominent accounts but is not a follower-count order. Pass a cursor back with the same sort it came from.'}, 'actor': {'type': 'string', 'pattern': '^(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]|@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|https:\\/\\/bsky\\.app\\/profile\\/(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-])\\/?(?:[?#].*)?)$', 'maxLength': 2048, 'minLength': 1, 'description': 'Handle (e.g. "alice.bsky.social") or DID of the account to query. A leading "@" and the account\'s bsky.app page ("https://bsky.app/profile/alice.bsky.social") are accepted and read as the handle or DID they carry. A bare name without a dot is not a handle â\x80\x94 use bsky_search_actors to resolve one.'}, 'limit': {'type': 'integer', 'default': 25, 'maximum': 100, 'minimum': 1, 'description': 'Maximum number of actors to return per page (1â\x80\x93100). Default 25.'}, 'cursor': {'type': 'string', 'maxLength': 2048, 'description': 'Opaque pagination cursor from a previous response. Omit for the first page.'}, 'direction': {'enum': ['followers', 'following'], 'type': 'string', 'description': '"followers" returns accounts that follow this actor. "following" returns accounts this actor follows.'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'anyOf': [{'not': {'required': ['error']}, 'required': ['actors', 'subject', 'totalReturned']}, {'required': ['error']}], '$schema': 'https://json-schema.org/draft/2020-12/schema', 'properties': {'cap': {'type': 'number', 'description': 'The limit applied to this page.'}, 'error': {'type': 'object', 'required': ['code', 'message'], 'properties': {'code': {'type': 'integer', 'maximum': 9007199254740991, 'minimum': -9007199254740991, 'description': 'JSON-RPC error code for this failure.'}, 'data': {'type': 'object', 'properties': {'reason': {'type': 'string', 'examples': ['actor_not_found'], 'description': 'Machine-readable failure mode. Declared by this tool: `actor_not_found`: The actor handle or DID does not resolve to an existing account. Other values are possible when a failure originates below the handler.'}, 'recovery': {'type': 'object', 'required': ['hint'], 'properties': {'hint': {'type': 'string'}}, 'description': 'Actionable next step for the caller.', 'additionalProperties': {}}, 'retryable': {'type': 'boolean', 'description': 'Whether retrying may succeed.'}}, 'additionalProperties': {}}, 'message': {'type': 'string', 'description': 'Human-readable description of what went wrong.'}}, 'description': 'Present when the call failed. Absent on success.', 'additionalProperties': {}}, 'shown': {'type': 'number', 'description': 'Number of actors returned on this page.'}, 'actors': {'type': 'array', 'items': {'type': 'object', 'required': ['did', 'handle'], 'properties': {'did': {'type': 'string', 'description': 'Decentralized Identifier of the actor.'}, 'avatar': {'type': 'string', 'description': 'Avatar image URL.'}, 'handle': {'type': 'string', 'description': 'Human-readable handle, e.g. "alice.bsky.social".'}, 'labels': {'type': 'array', 'items': {'type': 'object', 'required': ['val'], 'properties': {'val': {'type': 'string', 'description': 'Label value (content warning or moderation tag, e.g. "porn", "spam").'}}, 'description': 'A moderation label.', 'additionalProperties': False}, 'description': 'Moderation labels.'}, 'pronouns': {'type': 'string', 'description': 'Free-form pronouns the account set, e.g. "they/he". Absent when it set none. Account-authored text bounded only by length, not a fixed vocabulary â\x80\x94 read it as written rather than parsing it.'}, 'description': {'type': 'string', 'description': 'Biography / about text.'}, 'displayName': {'type': 'string', 'description': 'Display name set by the user.'}, 'verification': {'type': 'object', 'required': ['verifiedStatus', 'trustedVerifierStatus'], 'properties': {'verifiedStatus': {'type': 'string', 'description': 'Whether a trusted verifier verified this account: "valid", "invalid" (verified once, no longer holds), or "none". Passed through as Bluesky sends it, so another value may appear.'}, 'trustedVerifierStatus': {'type': 'string', 'description': 'Whether this account is itself a trusted verifier â\x80\x94 same values as verifiedStatus.'}}, 'description': 'Bluesky verification of this account â\x80\x94 what tells it from a look-alike handle. Absent when Bluesky sent none. Who issued it is on bsky_get_profile.', 'additionalProperties': False}}, 'description': 'A Bluesky actor in the social graph.', 'additionalProperties': False}, 'description': 'Actors in the requested direction of the social graph.'}, 'cursor': {'type': 'string', 'description': 'Opaque cursor for the next page. Absent on the last page.'}, 'notice': {'type': 'string', 'description': 'Guidance when the result set is empty or constrained.'}, 'subject': {'type': 'object', 'required': ['did', 'handle'], 'properties': {'did': {'type': 'string', 'description': 'Permanent DID of the queried account.'}, 'handle': {'type': 'string', 'description': 'Human-readable handle of the queried account.'}, 'pronouns': {'type': 'string', 'description': 'Free-form pronouns the subject account set, e.g. "they/he". Absent when it set none.'}, 'displayName': {'type': 'string', 'description': 'Subject display name.'}, 'verification': {'type': 'object', 'required': ['verifiedStatus', 'trustedVerifierStatus'], 'properties': {'verifiedStatus': {'type': 'string', 'description': 'Whether a trusted verifier verified this account: "valid", "invalid" (verified once, no longer holds), or "none". Passed through as Bluesky sends it, so another value may appear.'}, 'trustedVerifierStatus': {'type': 'string', 'description': 'Whether this account is itself a trusted verifier â\x80\x94 same values as verifiedStatus.'}}, 'description': 'Bluesky verification of this account â\x80\x94 what tells it from a look-alike handle. Absent when Bluesky sent none. Who issued it is on bsky_get_profile.', 'additionalProperties': False}}, 'description': 'Profile summary of the queried actor.', 'additionalProperties': False}, 'truncated': {'type': 'boolean', 'description': 'True when Bluesky returned a cursor, whatever this page held. A cursor is not proof more accounts exist: the next page can come back empty when the remaining accounts are unavailable.'}, 'totalReturned': {'type': 'number', 'description': 'Number of actors in this response page.'}}, 'additionalProperties': False}
bsky_get_post_quotes
Get Bluesky Post Quotes
Read the quote posts behind a Bluesky post's "quoteCount" — the posts that embed it with commentary of their own, newest first. On Bluesky this is where much of the reaction to a post lives; bsky_get_post_thread returns replies only. Accepts the post's AT-URI (at://<handle-or-did>/app.bsky.feed.post/<rkey>) from the "uri" field of any returned post, or its bsky.app URL (https://bsky.app/profile/<handle-or-did>/post/<rkey>); a handle costs one extra lookup. Returns each quote post with full text, author, engagement counts, and AT-URI. Each result's embed names the queried post by AT-URI and CID only — its text is not repeated on every result — and keeps any media the quoting post attached. "quoteCount" is an upper bound on what this returns: Bluesky's counter keeps quotes that have left the index. Supports cursor pagination.
Read only Open world Idempotent
Input schema
{'type': 'object', '$schema': 'https://json-schema.org/draft/2020-12/schema', 'required': ['uri'], 'properties': {'uri': {'type': 'string', 'pattern': '^(?:at:\\/\\/(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-])\\/app\\.bsky\\.feed\\.post\\/[a-zA-Z0-9._~:-]{1,512}|https:\\/\\/bsky\\.app\\/profile\\/(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-])\\/post\\/[a-zA-Z0-9._~:-]{1,512}\\/?(?:[?#].*)?)$', 'maxLength': 2048, 'description': 'The post whose quotes to read â\x80\x94 its AT-URI, e.g. "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3l6oveex3ii2l", or its bsky.app URL, e.g. "https://bsky.app/profile/bsky.app/post/3l6oveex3ii2l"; a trailing "/", "?â\x80¦", or "#â\x80¦" on the URL is ignored. Posts only â\x80\x94 a feed, profile, or list address is rejected.'}, 'limit': {'type': 'integer', 'default': 25, 'maximum': 100, 'minimum': 1, 'description': 'Maximum number of quote posts to return (1â\x80\x93100). Default 25. Pages often hold fewer than the limit and still continue â\x80\x94 follow the cursor, not the count. A page that would pass the 48,000-byte response budget comes back with fewer quotes and "budgetCapped: true"; its cursor continues from the first quote it left out.'}, 'cursor': {'type': 'string', 'maxLength': 2048, 'description': 'Opaque pagination cursor from a previous response for the same post, passed back unchanged. Omit for the first page.'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'anyOf': [{'not': {'required': ['error']}, 'required': ['uri', 'posts', 'totalReturned']}, {'required': ['error']}], '$schema': 'https://json-schema.org/draft/2020-12/schema', 'properties': {'cap': {'type': 'number', 'description': 'The limit applied to this page.'}, 'uri': {'type': 'string', 'description': 'AT-URI of the post whose quotes these are, in DID form â\x80\x94 the form Bluesky was asked in, whatever form the input took.'}, 'error': {'type': 'object', 'required': ['code', 'message'], 'properties': {'code': {'type': 'integer', 'maximum': 9007199254740991, 'minimum': -9007199254740991, 'description': 'JSON-RPC error code for this failure.'}, 'data': {'type': 'object', 'properties': {'reason': {'type': 'string', 'examples': ['post_not_found', 'invalid_cursor'], 'description': 'Machine-readable failure mode. Declared by this tool: `post_not_found`: No post exists at that address, or the handle in it does not resolve to an account. `invalid_cursor`: Bluesky could not continue from the cursor the request carried â\x80\x94 it answers a cursor it cannot decode with HTTP 500. Other values are possible when a failure originates below the handler.'}, 'recovery': {'type': 'object', 'required': ['hint'], 'properties': {'hint': {'type': 'string'}}, 'description': 'Actionable next step for the caller.', 'additionalProperties': {}}, 'retryable': {'type': 'boolean', 'description': 'Whether retrying may succeed.'}}, 'additionalProperties': {}}, 'message': {'type': 'string', 'description': 'Human-readable description of what went wrong.'}}, 'description': 'Present when the call failed. Absent on success.', 'additionalProperties': {}}, 'posts': {'type': 'array', 'items': {'type': 'object', 'required': ['uri', 'cid', 'text', 'author'], 'properties': {'cid': {'type': 'string', 'description': 'Content Identifier (CID) of the quote post record.'}, 'uri': {'type': 'string', 'description': 'AT-URI of the quote post, e.g. "at://did:plc:xxx/app.bsky.feed.post/yyy". Pass to bsky_get_post_thread for the replies to it, or back to this tool for its own quotes.'}, 'text': {'type': 'string', 'description': 'Full text of the quote post â\x80\x94 the commentary on the queried post.'}, 'embed': {'type': 'object', 'properties': {}, 'description': 'The embed on this quote post. On every result it is a "record" embed pointing at the queried post, cut back to what the quoting post itself carries: { uri, cid, recordKind?, media? } â\x80\x94 uri and cid address the queried post (the revision this quote points at), recordKind is set only when that post is unreadable ("notFound" | "blocked" | "detached"), and media is the image/video/link the quoting post attached alongside the quote. The queried post\'s own text, author, and attachments are not restated on each result. Other shapes, should an embed point elsewhere: images: { images: [{ url, alt }] }; external: { uri, title, description }; record: { uri, cid, text?, authorHandle?, embeds?, media?, omittedEmbeds?, recordKind? }; video: { playlist?, thumbnail?, presentation? }; unknown: { raw }.', 'additionalProperties': {}}, 'author': {'type': 'object', 'required': ['did', 'handle'], 'properties': {'did': {'type': 'string', 'description': 'Permanent DID of the author, e.g. "did:plc:z72i7hdynmk6r22z27h6tvur".'}, 'avatar': {'type': 'string', 'description': 'URL of the author avatar image.'}, 'handle': {'type': 'string', 'description': 'Human-readable handle of the author, e.g. "alice.bsky.social".'}, 'displayName': {'type': 'string', 'description': 'Display name set by the author.'}, 'verification': {'type': 'object', 'required': ['verifiedStatus', 'trustedVerifierStatus'], 'properties': {'verifiedStatus': {'type': 'string', 'description': 'Whether a trusted verifier verified the author: "valid", "invalid" (verified once, no longer holds), or "none". Passed through as Bluesky sends it, so another value may appear.'}, 'trustedVerifierStatus': {'type': 'string', 'description': 'Whether the author is itself a trusted verifier â\x80\x94 same values as verifiedStatus.'}}, 'description': 'Bluesky verification of the author â\x80\x94 what tells a verified account from a look-alike handle. Absent when Bluesky sent none. Who issued it is on bsky_get_profile.', 'additionalProperties': False}}, 'description': 'Author of this quote post.', 'additionalProperties': False}, 'labels': {'type': 'array', 'items': {'type': 'object', 'required': ['val'], 'properties': {'cts': {'type': 'string', 'description': 'ISO 8601 timestamp when the label was applied.'}, 'src': {'type': 'string', 'description': 'DID of the labeler that applied this label. Equal to the post author DID when the account labelled its own post, and a labeler service DID otherwise.'}, 'val': {'type': 'string', 'description': 'Label value (content warning or moderation tag, e.g. "porn", "spam").'}}, 'description': 'A moderation label applied by the AppView or a labeler service.', 'additionalProperties': False}, 'description': 'Moderation labels on this post.'}, 'createdAt': {'type': 'string', 'description': 'ISO 8601 timestamp when this post was created.'}, 'indexedAt': {'type': 'string', 'description': 'ISO 8601 timestamp when this post was indexed.'}, 'likeCount': {'type': 'number', 'description': 'Number of likes.'}, 'quoteCount': {'type': 'number', 'description': 'Number of quote posts Bluesky counts for this quote post â\x80\x94 read them with this tool. An upper bound on what it returns, since the counter keeps quotes that have left the index.'}, 'replyCount': {'type': 'number', 'description': 'Number of replies to this quote post.'}, 'replyToUri': {'type': 'string', 'description': 'AT-URI of the post this quote post replies to, if it is also a reply.'}, 'repostCount': {'type': 'number', 'description': 'Number of reposts.'}, 'replyRootUri': {'type': 'string', 'description': 'AT-URI of the post the conversation it replies in started from, if it is also a reply.'}}, 'description': 'A post quoting the queried post.', 'additionalProperties': False}, 'description': 'Posts quoting the queried post, newest first.'}, 'shown': {'type': 'number', 'description': 'Number of quote posts returned on this page.'}, 'cursor': {'type': 'string', 'description': 'Opaque cursor for the next page. Absent when there are no more quotes.'}, 'notice': {'type': 'string', 'description': 'Guidance on the page: that more quotes can be fetched with the returned cursor, or why the page is empty â\x80\x94 the post has no readable quotes, or the last page was reached.'}, 'truncated': {'type': 'boolean', 'description': 'True when Bluesky returned a cursor, whatever this page held â\x80\x94 pages often come back short of the limit with more behind them.'}, 'budgetCapped': {'type': 'boolean', 'description': 'True when a page of the requested limit would have passed this server\'s 48,000-byte response budget, so Bluesky was asked again for fewer quotes and that page was returned whole. The cursor comes from that same response, so paging on from it skips nothing. Independent of "truncated", which still means only that a cursor was returned.'}, 'totalReturned': {'type': 'number', 'description': 'Number of quote posts in this response page.'}}, 'additionalProperties': False}
bsky_get_post_thread
Get Bluesky Post Thread
Fetch the conversation for a post by AT-URI — the parent chain upward and the reply tree downward. Enter the thread at any point and traverse the discussion. AT-URIs have the format "at://<handle-or-did>/<collection>/<rkey>" and are returned in the "uri" field of every post the other post-returning tools emit, such as bsky_get_feed and bsky_get_author_feed; a bsky.app post URL (https://bsky.app/profile/<handle-or-did>/post/<rkey>) works as-is. Returns the root post, parent chain, and nested replies with per-post author and engagement data. Replies only: quote posts are not part of a thread — read them with bsky_get_post_quotes. The response is often a fraction of the conversation: Bluesky holds replies back past a per-post limit and offers no way to page the rest, so a thread with thousands of replies commonly returns a few hundred. Any node returning fewer replies than its own replyCount carries "truncated: true" with "unreturnedReplies" and a "truncationReason" — "depth" means the reply tree ended there and fetching that node's AT-URI as its own thread continues below it, "unavailable" means no request closes the gap. Read "unreturnedReplies" as an upper bound on what is missing rather than a count of readable replies: Bluesky's counter also includes replies that have left the index, so a small difference often means nothing is left to fetch. The parent chain is disclosed the same way: when it stops at parent_height instead of at the start of the conversation, the topmost node carries "parentChainTruncated: true" and fetching its AT-URI as its own thread continues upward. The enrichment fields total the difference for the whole thread; check them before describing a conversation as complete or naming its first post. A thread that would pass this server's 48,000-byte response budget is cut between whole posts — the target kept first, then its parents nearest-first, then replies level by level — with "budgetCapped: true" and the cut marked where it happened: "budgetOmittedReplyUris" on the target and "budgetOmittedReplies" on a kept reply name the replies left out, "budgetOmittedParents" on the topmost parent the ancestors; fetching those AT-URIs as their own threads reads the rest. In the rendered text nothing is indented: a reply's author heading carries how far it sits below the top-level reply it descends from ("### ↳2"), and every post also names its own parent on a "Reply to" line.
Read only Open world Idempotent
Input schema
{'type': 'object', '$schema': 'https://json-schema.org/draft/2020-12/schema', 'required': ['uri'], 'properties': {'uri': {'type': 'string', 'pattern': '^(?:at:\\/\\/(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-])\\/[a-zA-Z]+(?:\\.[a-zA-Z0-9-]+)+\\/[a-zA-Z0-9._~:-]{1,512}|https:\\/\\/bsky\\.app\\/profile\\/(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-])\\/post\\/[a-zA-Z0-9._~:-]{1,512}\\/?(?:[?#].*)?)$', 'maxLength': 2048, 'description': 'AT-URI of the post to fetch, e.g. "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/abc123". All three segments are required â\x80\x94 authority (handle or DID), collection, and record key. Obtain from the "uri" field of a post returned by bsky_get_feed or bsky_get_author_feed. The post\'s bsky.app page, e.g. "https://bsky.app/profile/bsky.app/post/abc123", is accepted and read as the AT-URI it names; a trailing "/", "?â\x80¦", or "#â\x80¦" on it is ignored.'}, 'depth': {'type': 'integer', 'default': 6, 'maximum': 10, 'minimum': 0, 'description': "How many levels of replies to include below the target post. Default 6, maximum 10 â\x80\x94 Bluesky itself returns no more than 10 levels however deep the request. Depth does not widen the reply tree either: the per-post reply limit is independent of it. To read below the deepest level returned, fetch an edge node's AT-URI as its own thread."}, 'parent_height': {'type': 'integer', 'default': 80, 'maximum': 100, 'minimum': 0, 'description': 'How many parent posts to include in the parent chain above the target post. Default 80, maximum 100. The chain is returned level for level up to this many posts and stops early at the conversation root. When it stops at this bound instead, the topmost node carries "parentChainTruncated: true" â\x80\x94 fetch that node\'s AT-URI as its own thread to read above it. Set to 0 to skip the chain entirely; a reply target then reports the same marker on itself, since its own parent was not returned either.'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'anyOf': [{'not': {'required': ['error']}, 'required': ['thread', 'totalReturned']}, {'required': ['error']}], '$schema': 'https://json-schema.org/draft/2020-12/schema', 'properties': {'error': {'type': 'object', 'required': ['code', 'message'], 'properties': {'code': {'type': 'integer', 'maximum': 9007199254740991, 'minimum': -9007199254740991, 'description': 'JSON-RPC error code for this failure.'}, 'data': {'type': 'object', 'properties': {'reason': {'type': 'string', 'examples': ['invalid_at_uri', 'post_not_found', 'uri_is_feed'], 'description': 'Machine-readable failure mode. Declared by this tool: `invalid_at_uri`: The AppView rejected the AT-URI â\x80\x94 the shape passed the input pattern but the authority, collection, or record key is not one it can resolve. `post_not_found`: The AT-URI is well-formed but the post was deleted or never existed. `uri_is_feed`: The AT-URI names a feed generator (app.bsky.feed.generator), not a post. Other values are possible when a failure originates below the handler.'}, 'recovery': {'type': 'object', 'required': ['hint'], 'properties': {'hint': {'type': 'string'}}, 'description': 'Actionable next step for the caller.', 'additionalProperties': {}}, 'retryable': {'type': 'boolean', 'description': 'Whether retrying may succeed.'}}, 'additionalProperties': {}}, 'message': {'type': 'string', 'description': 'Human-readable description of what went wrong.'}}, 'description': 'Present when the call failed. Absent on success.', 'additionalProperties': {}}, 'notice': {'type': 'string', 'description': 'What this response is missing, how much of the gap is explained, and which part of it can still be reached by a further request.'}, 'thread': {'type': 'object', 'properties': {}, 'description': 'The conversation thread rooted at the requested post â\x80\x94 a recursive node tree. Each node has: post: { uri, cid, text, author: { did, handle, displayName?, avatar?, verification? }, replyCount?, repostCount?, likeCount?, quoteCount?, indexedAt?, createdAt?, labels?: [{ val, src?, cts? }], embed?, replyToUri?, replyRootUri? }. author.verification: { verifiedStatus, trustedVerifierStatus } â\x80\x94 whether a trusted verifier verified the author and whether the author is one, each "valid", "invalid" (verified once, no longer holds), or "none", passed through as Bluesky sends it; absent when Bluesky sent none. quoteCount counts quote posts, which are not part of the thread â\x80\x94 read them with bsky_get_post_quotes. parent?: parent thread node. replies?: array of child thread nodes. truncated?: true when the node\'s own post.replyCount exceeds the replies returned for it, with unreturnedReplies: the size of that difference, and truncationReason: "depth" (the reply tree ends at this node â\x80\x94 fetch its post.uri as its own thread to continue below it) or "unavailable" (no request closes the gap). unreturnedReplies is an upper bound on what is missing, not a count of readable replies: Bluesky\'s counter keeps including replies that have left the index, so a node reporting one unreturned reply often has none left to fetch. Only reply-tree nodes carry these; a parent-chain node is linear by construction and never reports a reply shortfall. parentChainTruncated?: true on the topmost node above the target when the chain stopped at parent_height rather than at the start of the conversation â\x80\x94 that node is a reply to a post this response does not contain, so it is not the conversation root. Fetch that node\'s post.uri as its own thread to continue upward; parent_height is honored level for level, so the ancestors above it are one request away. Set on the target itself when no parent was returned at all. notFound?: true when the post was deleted or never existed. blocked?: true when its author blocks this view. Both stubs carry the reported AT-URI on post.uri and no content â\x80\x94 a blocked node also carries the author DID on post.author.did. Set only when this server\'s 48,000-byte response budget cut the thread (budgetCapped), on the posts Bluesky did return: budgetOmittedReplyUris?: on the target, the AT-URIs of its direct replies left out, in Bluesky order â\x80\x94 fetch each as its own thread (parent_height 0) to read it and everything below it. budgetOmittedReplies?: on any other node, how many of its direct replies were left out, each with everything below it â\x80\x94 fetch the node\'s post.uri as its own thread (parent_height 0). budgetOmittedParents?: on the topmost parent kept, or the target when none was, how many ancestors above it were left out â\x80\x94 fetch the node\'s post.uri with depth 0 to read them. Independent of truncated / unreturnedReplies / parentChainTruncated, which describe what Bluesky itself did not return.', 'additionalProperties': {}}, 'truncated': {'type': 'boolean', 'description': 'True when at least one post in the reply tree returned fewer replies than Bluesky counts for it â\x80\x94 counted over every post Bluesky returned, including any the response budget left out.'}, 'threadgate': {'type': 'object', 'required': ['uri', 'hiddenReplies'], 'properties': {'uri': {'type': 'string', 'description': 'AT-URI of the threadgate record itself.'}, 'allow': {'type': 'array', 'items': {'enum': ['follower', 'following', 'list', 'mentioned', 'unknown'], 'type': 'string'}, 'description': 'Who may reply. Omitted when anyone may; an empty array means the author turned replies off. Replies posted before the rule was set stay in the thread.'}, 'hiddenReplies': {'type': 'array', 'items': {'type': 'string'}, 'description': 'AT-URIs of replies the thread author hid. Some are still present in the returned tree â\x80\x94 compare against the node URIs rather than assuming every entry is absent.'}}, 'description': "The thread author's reply restrictions, present only when they set one. Hidden replies are counted in replyCount whether or not they were returned, so a gated thread is one reason the counts run ahead of the tree.", 'additionalProperties': False}, 'budgetCapped': {'type': 'boolean', 'description': 'True when the whole thread would have passed this server\'s 48,000-byte response budget, so posts Bluesky returned were left out whole â\x80\x94 the target first kept, then its parents nearest-first, then replies level by level. The nodes at the cut carry budgetOmittedReplyUris, budgetOmittedReplies, or budgetOmittedParents, and fetching those AT-URIs reads everything left out. Independent of "truncated" and "parentChainTruncated", which describe what Bluesky did not return.'}, 'budgetOmitted': {'type': 'number', 'description': 'How many posts Bluesky returned that the response budget left out, set alongside budgetCapped. totalReturned counts the posts kept.'}, 'totalReturned': {'type': 'number', 'description': 'Thread nodes in this response â\x80\x94 the target post, its parent chain, and every reply returned.'}, 'unreturnedReplies': {'type': 'number', 'description': "How far the reply counts run ahead of the replies returned, summed across the reply tree. An upper bound on what is missing, not a count of readable replies â\x80\x94 Bluesky's counters keep including replies that have left the index. Summed over every post Bluesky returned, including any the response budget left out. Compare against the root post replyCount to judge how much of the conversation is present."}, 'parentChainTruncated': {'type': 'boolean', 'description': 'True when the parent chain stopped at parent_height instead of reaching the start of the conversation, so the topmost post returned above the target is not the conversation root. Independent of "truncated", which covers the reply tree, and unlike it fully recoverable: fetch the topmost parent\'s AT-URI as its own thread to continue upward.'}}, 'additionalProperties': False}
bsky_get_profile
Get Bluesky Profile
Fetch a Bluesky actor's public profile by handle (e.g. "bsky.app") or DID (e.g. "did:plc:z72i7hdynmk6r22z27h6tvur"). Returns displayName, handle, DID, bio, pronouns, website, follower/following/post counts, avatar URL, moderation labels, pinned post AT-URI, and Bluesky verification — whether the account is verified or a trusted verifier, and who verified it. Use this as the first step to resolve a handle to a DID before calling tools that require a DID or AT-URI. Handles and DIDs are interchangeable as input, and "@bsky.app" or the account's bsky.app page (https://bsky.app/profile/bsky.app) work as-is.
Read only Open world Idempotent
Input schema
{'type': 'object', '$schema': 'https://json-schema.org/draft/2020-12/schema', 'required': ['actor'], 'properties': {'actor': {'type': 'string', 'pattern': '^(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]|@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|https:\\/\\/bsky\\.app\\/profile\\/(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-])\\/?(?:[?#].*)?)$', 'maxLength': 2048, 'minLength': 1, 'description': 'Handle (e.g. "bsky.app", "alice.bsky.social") or DID (e.g. "did:plc:z72i7hdynmk6r22z27h6tvur") of the actor to look up. A leading "@" ("@bsky.app") and the account\'s bsky.app page ("https://bsky.app/profile/bsky.app") are accepted and read as the handle or DID they carry. A bare name without a dot is not a handle â\x80\x94 use bsky_search_actors to resolve one.'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'anyOf': [{'not': {'required': ['error']}, 'required': ['did', 'handle']}, {'required': ['error']}], '$schema': 'https://json-schema.org/draft/2020-12/schema', 'properties': {'did': {'type': 'string', 'description': 'Decentralized Identifier â\x80\x94 the permanent, portable identity key for this account.'}, 'error': {'type': 'object', 'required': ['code', 'message'], 'properties': {'code': {'type': 'integer', 'maximum': 9007199254740991, 'minimum': -9007199254740991, 'description': 'JSON-RPC error code for this failure.'}, 'data': {'type': 'object', 'properties': {'reason': {'type': 'string', 'examples': ['actor_not_found'], 'description': 'Machine-readable failure mode. Declared by this tool: `actor_not_found`: The handle does not resolve or the profile does not exist. Other values are possible when a failure originates below the handler.'}, 'recovery': {'type': 'object', 'required': ['hint'], 'properties': {'hint': {'type': 'string'}}, 'description': 'Actionable next step for the caller.', 'additionalProperties': {}}, 'retryable': {'type': 'boolean', 'description': 'Whether retrying may succeed.'}}, 'additionalProperties': {}}, 'message': {'type': 'string', 'description': 'Human-readable description of what went wrong.'}}, 'description': 'Present when the call failed. Absent on success.', 'additionalProperties': {}}, 'avatar': {'type': 'string', 'description': 'URL of the profile avatar image.'}, 'handle': {'type': 'string', 'description': 'Human-readable username, e.g. "alice.bsky.social".'}, 'labels': {'type': 'array', 'items': {'type': 'object', 'required': ['val'], 'properties': {'cts': {'type': 'string', 'description': 'ISO 8601 timestamp when the label was applied.'}, 'src': {'type': 'string', 'description': 'DID of the labeler that applied this label.'}, 'val': {'type': 'string', 'description': 'Label value (content warning or moderation tag, e.g. "porn", "spam").'}}, 'description': 'A moderation label applied by the AppView or a labeler service.', 'additionalProperties': False}, 'description': 'Moderation labels applied to this profile.'}, 'website': {'type': 'string', 'description': 'URL the account set as its website, in the profile field of that name rather than in the bio. Absent when it set none. The one link on a profile that points somewhere else â\x80\x94 follow it before reading the bio for one.'}, 'pronouns': {'type': 'string', 'description': 'Free-form pronouns the account set, e.g. "they/he". Absent when it set none. Account-authored text bounded only by length, not a fixed vocabulary â\x80\x94 read it as written rather than parsing it.'}, 'createdAt': {'type': 'string', 'description': 'ISO 8601 timestamp of account creation.'}, 'indexedAt': {'type': 'string', 'description': 'ISO 8601 timestamp when the AppView last indexed this profile.'}, 'postsCount': {'type': 'number', 'description': 'Total posts authored by this actor.'}, 'description': {'type': 'string', 'description': 'Biography / about text.'}, 'displayName': {'type': 'string', 'description': 'Display name set by the user. May differ from the handle.'}, 'followsCount': {'type': 'number', 'description': 'Number of accounts this actor follows.'}, 'verification': {'type': 'object', 'required': ['verifiedStatus', 'trustedVerifierStatus', 'verifications'], 'properties': {'verifications': {'type': 'array', 'items': {'type': 'object', 'required': ['issuer', 'uri', 'isValid', 'createdAt'], 'properties': {'uri': {'type': 'string', 'description': 'AT-URI of the verification record.'}, 'issuer': {'type': 'string', 'description': 'DID of the trusted verifier that issued it.'}, 'isValid': {'type': 'boolean', 'description': 'Whether this verification still holds.'}, 'createdAt': {'type': 'string', 'description': 'ISO 8601 timestamp when it was issued.'}, 'issuerHandle': {'type': 'string', 'description': 'Handle of the issuer, when Bluesky sent it.'}, 'issuerDisplayName': {'type': 'string', 'description': 'Display name of the issuer, when Bluesky sent it. Account-authored text.'}}, 'description': 'One verification a trusted verifier issued for this account.', 'additionalProperties': False}, 'description': 'Verifications issued by trusted verifiers â\x80\x94 empty for an account that verifies others but was never verified itself.'}, 'verifiedStatus': {'type': 'string', 'description': 'Whether a trusted verifier verified this account: "valid", "invalid" (verified once, no longer holds), or "none". Passed through as Bluesky sends it, so another value may appear.'}, 'trustedVerifierStatus': {'type': 'string', 'description': 'Whether this account is itself a trusted verifier, whose verifications Bluesky honors â\x80\x94 same values as verifiedStatus.'}}, 'description': 'Bluesky verification state â\x80\x94 what tells a verified account from a look-alike handle. Absent when Bluesky sent none, which it does for an account neither verified nor a trusted verifier.', 'additionalProperties': False}, 'pinnedPostUri': {'type': 'string', 'description': 'AT-URI of the pinned post, if any. Pass to bsky_get_post_thread to read it.'}, 'followersCount': {'type': 'number', 'description': 'Number of accounts following this actor.'}}, 'additionalProperties': False}
bsky_get_trending
Get Bluesky Trending Topics
Fetch the current real-time trending topics on Bluesky. Returns topics with display name, Bluesky's one-sentence summary of the story, post count, category (politics, sports, pop-culture, etc.), status, start time, and the representative accounts driving each topic — so "who is talking about this" needs no follow-up call. Entry point for "what is Bluesky talking about right now". Each trend is a feed: pass its feedUri to bsky_get_feed to read the trend's posts. Note: uses the app.bsky.unspecced.getTrends endpoint, which is not part of Bluesky's stable lexicon and may change without notice.
Read only Open world
Input schema
{'type': 'object', '$schema': 'https://json-schema.org/draft/2020-12/schema', 'properties': {'limit': {'type': 'integer', 'default': 10, 'maximum': 25, 'minimum': 1, 'description': "Maximum number of trending topics to return (1â\x80\x9325). Default 10. 25 is Bluesky's maximum."}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'anyOf': [{'not': {'required': ['error']}, 'required': ['trends', 'totalReturned']}, {'required': ['error']}], '$schema': 'https://json-schema.org/draft/2020-12/schema', 'properties': {'cap': {'type': 'number', 'description': 'The limit applied to this request.'}, 'error': {'type': 'object', 'required': ['code', 'message'], 'properties': {'code': {'type': 'integer', 'maximum': 9007199254740991, 'minimum': -9007199254740991, 'description': 'JSON-RPC error code for this failure.'}, 'data': {'type': 'object', 'properties': {'reason': {'type': 'string', 'description': 'Machine-readable failure mode.'}, 'recovery': {'type': 'object', 'required': ['hint'], 'properties': {'hint': {'type': 'string'}}, 'description': 'Actionable next step for the caller.', 'additionalProperties': {}}, 'retryable': {'type': 'boolean', 'description': 'Whether retrying may succeed.'}}, 'additionalProperties': {}}, 'message': {'type': 'string', 'description': 'Human-readable description of what went wrong.'}}, 'description': 'Present when the call failed. Absent on success.', 'additionalProperties': {}}, 'shown': {'type': 'number', 'description': 'Number of trending topics returned.'}, 'notice': {'type': 'string', 'description': 'Guidance when the result set is empty or constrained.'}, 'trends': {'type': 'array', 'items': {'type': 'object', 'required': ['topic', 'displayName'], 'properties': {'link': {'type': 'string', 'description': "The trend feed's page on bsky.app (https://bsky.app/profile/â\x80¦/feed/â\x80¦), if provided."}, 'topic': {'type': 'string', 'description': 'Record key of the feed generator behind this trend, e.g. "1d558a3bc9ff" â\x80\x94 an identifier, not a search term. Read the trend\'s posts through feedUri.'}, 'actors': {'type': 'array', 'items': {'type': 'object', 'required': ['did', 'handle'], 'properties': {'did': {'type': 'string', 'description': 'Permanent DID of the actor.'}, 'handle': {'type': 'string', 'description': 'Human-readable handle, e.g. "alice.bsky.social".'}, 'displayName': {'type': 'string', 'description': 'Display name set by the actor.'}}, 'description': 'A representative account posting about this topic.', 'additionalProperties': False}, 'description': 'Representative accounts posting about this topic â\x80\x94 the AppView returns five per trend. Pass a handle to bsky_get_author_feed or bsky_get_profile instead of searching for authors.'}, 'status': {'type': 'string', 'description': 'Velocity signal as Bluesky reports it, e.g. "hot", "cooling", or "stale".'}, 'feedUri': {'type': 'string', 'description': "AT-URI of the feed that collects this trend's posts â\x80\x94 pass it to bsky_get_feed as-is to read them. Parsed from link; absent when link is missing or is not a feed page."}, 'category': {'type': 'string', 'description': 'Category of the trend, e.g. "politics", "sports", "pop-culture".'}, 'postCount': {'type': 'number', 'description': 'Approximate number of posts about this topic.'}, 'startedAt': {'type': 'string', 'description': 'ISO 8601 timestamp when this topic started trending.'}, 'description': {'type': 'string', 'description': "Bluesky's one-sentence summary of the story behind the trend. Third-party text, rendered quoted."}, 'displayName': {'type': 'string', 'description': 'Human-readable topic name, e.g. "AI Launch 2025".'}}, 'description': 'A single real-time trending topic on Bluesky.', 'additionalProperties': False}, 'description': 'Current trending topics, ordered by prominence.'}, 'truncated': {'type': 'boolean', 'description': "True when more topics were trending than limit â\x80\x94 raising limit shows them. Never set at limit 25, Bluesky's maximum."}, 'totalReturned': {'type': 'number', 'description': 'Number of trending topics returned.'}}, 'additionalProperties': False}
bsky_search_actors
Search Bluesky Actors
Find Bluesky accounts by name or handle fragment. Returns ranked profiles with handle, DID, displayName, bio, pronouns when the account set them, and Bluesky verification status — which tells a verified account from a look-alike handle. Follower, following, and post counts and the website are not on this view — bsky_get_profile returns them for one account. Use before bsky_get_profile or bsky_get_author_feed when you have a name but not a confirmed handle. Supports cursor-based pagination for browsing beyond the first page of results.
Read only Open world Idempotent
Input schema
{'type': 'object', '$schema': 'https://json-schema.org/draft/2020-12/schema', 'required': ['query'], 'properties': {'limit': {'type': 'integer', 'default': 25, 'maximum': 100, 'minimum': 1, 'description': 'Maximum number of actors to return (1â\x80\x93100). Default 25.'}, 'query': {'type': 'string', 'pattern': '\\S', 'maxLength': 500, 'minLength': 1, 'description': 'Name or handle fragment to search for, e.g. "alice" or "nytimes.com". Must not be blank.'}, 'cursor': {'type': 'string', 'maxLength': 2048, 'description': 'Opaque pagination cursor from a previous response to the same query. Omit for the first page.'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'anyOf': [{'not': {'required': ['error']}, 'required': ['actors', 'totalReturned']}, {'required': ['error']}], '$schema': 'https://json-schema.org/draft/2020-12/schema', 'properties': {'cap': {'type': 'number', 'description': 'The limit applied to this page.'}, 'error': {'type': 'object', 'required': ['code', 'message'], 'properties': {'code': {'type': 'integer', 'maximum': 9007199254740991, 'minimum': -9007199254740991, 'description': 'JSON-RPC error code for this failure.'}, 'data': {'type': 'object', 'properties': {'reason': {'type': 'string', 'examples': ['invalid_cursor'], 'description': 'Machine-readable failure mode. Declared by this tool: `invalid_cursor`: Bluesky could not continue from the cursor the request carried â\x80\x94 it answers a cursor it cannot decode with HTTP 400. Other values are possible when a failure originates below the handler.'}, 'recovery': {'type': 'object', 'required': ['hint'], 'properties': {'hint': {'type': 'string'}}, 'description': 'Actionable next step for the caller.', 'additionalProperties': {}}, 'retryable': {'type': 'boolean', 'description': 'Whether retrying may succeed.'}}, 'additionalProperties': {}}, 'message': {'type': 'string', 'description': 'Human-readable description of what went wrong.'}}, 'description': 'Present when the call failed. Absent on success.', 'additionalProperties': {}}, 'shown': {'type': 'number', 'description': 'Number of actors returned on this page.'}, 'actors': {'type': 'array', 'items': {'type': 'object', 'required': ['did', 'handle'], 'properties': {'did': {'type': 'string', 'description': 'Decentralized Identifier â\x80\x94 permanent portable identity key.'}, 'avatar': {'type': 'string', 'description': 'URL of the profile avatar image.'}, 'handle': {'type': 'string', 'description': 'Human-readable username, e.g. "alice.bsky.social".'}, 'labels': {'type': 'array', 'items': {'type': 'object', 'required': ['val'], 'properties': {'src': {'type': 'string', 'description': 'DID of the labeling service.'}, 'val': {'type': 'string', 'description': 'Label value (content warning or moderation tag).'}}, 'description': 'A moderation label applied to this actor.', 'additionalProperties': False}, 'description': 'Moderation labels applied to this actor.'}, 'pronouns': {'type': 'string', 'description': 'Free-form pronouns the account set, e.g. "they/he". Absent when it set none. Account-authored text bounded only by length, not a fixed vocabulary â\x80\x94 read it as written rather than parsing it.'}, 'description': {'type': 'string', 'description': 'Biography / about text.'}, 'displayName': {'type': 'string', 'description': 'Display name set by the user.'}, 'verification': {'type': 'object', 'required': ['verifiedStatus', 'trustedVerifierStatus'], 'properties': {'verifiedStatus': {'type': 'string', 'description': 'Whether a trusted verifier verified this account: "valid", "invalid" (verified once, no longer holds), or "none". Passed through as Bluesky sends it, so another value may appear.'}, 'trustedVerifierStatus': {'type': 'string', 'description': 'Whether this account is itself a trusted verifier â\x80\x94 same values as verifiedStatus.'}}, 'description': 'Bluesky verification of this account â\x80\x94 what tells it from a look-alike handle. Absent when Bluesky sent none. Who issued it is on bsky_get_profile.', 'additionalProperties': False}}, 'description': 'A Bluesky actor profile summary.', 'additionalProperties': False}, 'description': 'Matching actor profiles, ranked by relevance.'}, 'cursor': {'type': 'string', 'description': 'Opaque cursor for the next page â\x80\x94 pass it back with the same query. Absent on the last page.'}, 'notice': {'type': 'string', 'description': 'Guidance when the result set is empty or constrained.'}, 'truncated': {'type': 'boolean', 'description': 'True when Bluesky returned a cursor for another page, whatever this page held â\x80\x94 pages often hold fewer actors than limit and still continue.'}, 'totalReturned': {'type': 'number', 'description': 'Number of actors in this response page.'}}, 'additionalProperties': False}
Removed
bsky_search_posts
Sept. 27, 2026, 2:44 a.m.
Changed
bsky_get_follows
Sept. 27, 2026, 2:44 a.m.
Added
bsky_get_post_quotes
Sept. 27, 2026, 2:44 a.m.
Changed
bsky_get_post_thread
Sept. 27, 2026, 2:44 a.m.
Changed
bsky_get_author_feed
Sept. 27, 2026, 2:44 a.m.
Added
bsky_get_feed
Sept. 27, 2026, 2:44 a.m.
Changed
bsky_get_trending
Sept. 27, 2026, 2:44 a.m.
Changed
bsky_search_actors
Sept. 27, 2026, 2:44 a.m.
Changed
bsky_get_profile
Sept. 27, 2026, 2:44 a.m.
Changed
bsky_get_post_thread
Sept. 23, 2026, 2:42 a.m.
Added
bsky_get_follows
Sept. 17, 2026, 12:41 p.m.
Added
bsky_get_post_thread
Sept. 17, 2026, 12:41 p.m.
Added
bsky_search_posts
Sept. 17, 2026, 12:41 p.m.
Added
bsky_get_author_feed
Sept. 17, 2026, 12:41 p.m.
Added
bsky_get_trending
Sept. 17, 2026, 12:41 p.m.
Added
bsky_search_actors
Sept. 17, 2026, 12:41 p.m.
Added
bsky_get_profile
Sept. 17, 2026, 12:41 p.m.