@cyanheads/bluesky-mcp-server
MCP serverSearchYour AI can search Bluesky once this integration is added. It can find posts, profiles, feeds, threads, and trending topics, so you can keep up with what is happening on Bluesky without leaving your chat.
Use @cyanheads/bluesky-mcp-server in Claude, ChatGPT or Ahel Desktop
Free. Sign in, add @cyanheads/bluesky-mcp-server and connect your AI. About a minute.
Also: Claude Code · Cursor · Codex
Then ask your AI: use the bsky get profile tool from @cyanheads/bluesky-mcp-server
No key or account is asked for. Not verified by us yet: Ahel has not run a call through it.
Details
Available today. Use it from your connected AI after setup.
No key or account is asked for. Not verified by us yet: Ahel has not run a call through it.
After adding it, ask your AI to search Bluesky for a topic, person, or trend you are curious about.
What your AI can do with it
- Search posts on Bluesky
- Look up profiles on Bluesky
- Search feeds on Bluesky
- Pull up threads to see full conversations
- See trending topics on Bluesky
Install @cyanheads/bluesky-mcp-server
The server’s own address, for the clients that take one directly. Or connect Ahel once and every client you use reads it from one address, with the account kept on Ahel rather than in each client’s config.
Claude Code
claude mcp add --transport http --scope user cyanheads-bluesky-mcp-server 'https://bluesky.caseyjhand.com/mcp'Run it once in your project, then open /mcp to approve any sign-in the server asks for.
Claude Desktop
https://bluesky.caseyjhand.com/mcpAdd a custom connector in Settings, paste this address, and approve the sign-in.
Cursor
cursor://anysphere.cursor-deeplink/mcp/install?name=cyanheads-bluesky-mcp-server&config=eyJ1cmwiOiJodHRwczovL2JsdWVza3kuY2FzZXlqaGFuZC5jb20vbWNwIn0=Open the link and Cursor adds the server at that address.
ChatGPT
https://bluesky.caseyjhand.com/mcpIn Settings, enable Developer mode, create an MCP app, and paste this address. Your plan and workspace must allow custom apps.
Codex
codex mcp add cyanheads-bluesky-mcp-server --url 'https://bluesky.caseyjhand.com/mcp'Run it once, then sign in with codex mcp login cyanheads-bluesky-mcp-server if the server asks for an account.
From the project's README
As published by cyanheads/bluesky-mcp-server in README.md.
Public Hosted Server: https://bluesky.caseyjhand.com/mcp
Overview
Bluesky data over the AT Protocol AppView. Resolve profiles, track trending topics and read the feeds behind them, walk custom feeds, author feeds, threads, and the quote posts on a post — all without an account — and add full-text post search with an optional app password. Shared bsky.app links and @handles work wherever the matching account, post, or feed is asked for. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
Tools
| Tool | Description |
|---|---|
bsky_search_posts | Full-text search across public Bluesky posts, with author, mention, language, tag, domain, URL, date, and sort filters — offered only when an app password is configured |
bsky_get_profile | Fetch a Bluesky actor's public profile by handle or DID — the handle↔DID resolver |
bsky_get_feed | Read a feed generator's posts — a trend's feed, Discover, or any custom feed — by AT-URI or bsky.app URL |
bsky_get_author_feed | A user's recent posts ordered newest-first, filterable by post type |
bsky_get_post_thread | Fetch the conversation for a post by AT-URI or bsky.app URL — parent chain upward and reply tree downward, with what Bluesky counted but did not return |
bsky_get_post_quotes | Read the quote posts behind a post's quoteCount, newest first — where much of the reaction to a post lives |
bsky_search_actors | Find Bluesky accounts by name or handle fragment |
bsky_get_follows | Paginated social graph edges — who a user follows or who follows them |
bsky_get_trending | Real-time trending topics on Bluesky with a story summary, post count, category, status, the accounts driving each topic, and the feed that collects its posts |
Resources
| Resource | Description |
|---|---|
bsky://profile/{actor} | A Bluesky actor's public profile, addressable by handle or DID |
All resource data is also reachable via tools. Use bsky_get_profile for programmatic access or bsky://profile/{actor} to inject profile context directly.
Capability reference
bsky_search_posts tool
- Bluesky refuses post search without a signed-in account, so this tool is registered only when
BLUESKY_IDENTIFIERandBLUESKY_APP_PASSWORDare set — without them it is absent fromtools/list(see Post search) - Searches run as that account through its PDS; the first search logs in, the session is reused and refreshed, and a rejected login is not retried. Failures surface as
search_auth_failed(the login or its renewal was rejected),search_login_limited(the account's daily login limit is used up — the error names when searching resumes), orsearch_refused(Bluesky refused the search) - Filters: author and mentioned account (handle or DID, also as
@handleor a bsky.app profile URL —mentionsmatches rich-text mentions only), two-letter language code, hashtag (with or without#), linkeddomain(bare hostname, leadingwww.dropped) or exacturl(in text links or link cards),since/until, andtop/latestsort; up to 100 results per call via opaque cursor pagination since/untilcompare against each post's sort time — the earlier ofcreatedAtandindexedAt— to the whole second, inclusive; a date alone is 00:00:00 UTC, sountil: "2026-01-01"covers all of 2025-12-31- Identifier, language, domain, URL, and date inputs are pattern-validated locally before the upstream call. A three-letter language code (
"fil","eng") is rejected, since Bluesky search ignores one and returns unfiltered results; the code is sent lowercased, and later subtags are accepted but ignored upstream (en-USfilters asen) - When Bluesky rejects a parameter, its own explanation is surfaced via the
upstream_rejected_filtererror reason instead of a bare status code; a cursor it can't decode fails asinvalid_cursor, after one request hitsTotalis Bluesky's estimate, never an exact count: below 10,000 it is an upper bound on what paging returns (Bluesky counts before dropping posts it won't return), and exactly 10,000 — the cap — means "at least that many";truncated/shown/capare keyed on the returned cursor, which Bluesky omits once nothing more matches- A page that would pass the 48,000-byte response budget on either surface is searched again at the
limitthat fits and returned whole —budgetCapped: true, withhitsTotaland the cursor from that one response - Embeds normalize to a
type-discriminated union (images,external,record,video,unknown); a quoted post carries its own attachments up to 3 nesting levels, withomittedEmbedscounting what went deeper andrecordKindnaming a quote that's deleted, blocked, detached, or not a post - Moderation labels are surfaced as-is, unfiltered
bsky_get_feed tool
- Takes a feed generator 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>, with any trailing/,?…, or#…ignored); a trend'sfeedUriworks as-is, and Discover isat://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot - A handle owner costs one extra lookup (
resolveHandle), a DID owner none; any other collection, such as a post AT-URI, is rejected before the upstream call - A post the feed pinned to its top carries
pinned: true; a repost carriesrepostedBy/repostedAt - Up to 100 posts per call, paginated via cursor; truncation is disclosed on the cursor, since a feed can return fewer than
limitwith more behind it - A page that would pass the 48,000-byte response budget is asked for again at the
limitthat fits and returned whole, markedbudgetCapped: true; a ranked feed reranks on every request, so its posts and cursor still come from one response feed_not_found(no such feed or handle),feed_unavailable(the feed's generator did not answer — not retried, so a down feed fails fast),feed_requires_login(a personalized feed)- Always unauthenticated, even when search credentials are set
bsky_get_profile tool
- Accepts a handle or DID, also as
@handleor the account's bsky.app page (https://bsky.app/profile/<handle-or-did>); returns displayName, handle, DID, bio, pronouns, website, follower/following/post counts, avatar, moderation labels, and pinned post AT-URI websiteis the one link carried in its own field rather than inside the bio; both it andpronounsare absent when the account set neitherverificationcarries Bluesky's full verification state —verifiedStatusandtrustedVerifierStatus(valid,invalid, ornone, passed through verbatim) and every verification a trusted verifier issued, with its issuer, validity, date, and record AT-URI; absent when Bluesky sent none- The bio renders as a markdown blockquote in
content[], since it's account-authored text that can carry its own markdown structure actor_not_foundwhen the handle doesn't resolve — resolve the handle withbsky_search_actorsfirst- The primary handle↔DID resolver — use before tools that require a DID or AT-URI when only a handle is known
bsky_get_author_feed tool
- Takes a handle or DID, also as
@handleor the account's bsky.app page filter:posts_with_replies,posts_no_replies(default, excludes replies), andposts_and_author_threadsinclude reposts;posts_with_media(the actor's own posts with images or video, no link cards) andposts_with_videoreturn noneinclude_pins: trueadds the profile's pinned post, markedpinned: true, first on the first page — in addition tolimit, and whether or not it matchesfilter- Reposts carry
repostedByandrepostedAt;authoralways names who actually wrote the post - Under the filters that include reposts,
limitcounts them too, so a heavily-reposting account can return far fewer of its own posts than the limit suggests;originalPosts/repostsreport the actual split whenever a repost is present - Up to 100 posts per call, paginated via cursor; a page that would pass the 48,000-byte response budget is asked for again at the
limitthat fits (the pin not counted), markedbudgetCapped: true, and its cursor resumes at the first post left out actor_not_foundwhen the handle or DID doesn't resolve;invalid_cursor, after one request, when Bluesky can't decode the cursor passed (it answers those with HTTP 500, which is otherwise retried)
bsky_get_post_thread tool
- Takes a post AT-URI or its bsky.app page (
https://bsky.app/profile/<handle-or-did>/post/<rkey>, trailing/,?…, or#…ignored) - Replies only — quote posts live under
bsky_get_post_quotes depth(reply levels, default 6, max 10 — Bluesky's own ceiling, however deep the request) andparent_height(parent chain height, default 80, max 100)- A node returning fewer replies than its own
replyCountcarriestruncated: true,unreturnedReplies(an upper bound, not an exact shortfall), andtruncationReason("depth"— fetch that node's AT-URI to continue, or"unavailable"— no request closes the gap) - When the parent chain stops at
parent_heightshort of the conversation root, the topmost node carriesparentChainTruncated: true— recoverable by fetching that node's AT-URI as its own thread - A thread that would pass the 48,000-byte response budget keeps the target, then parents nearest-first, then replies level by level, and cuts between whole posts:
budgetCappedandbudgetOmitted(posts left out) in enrichment,budgetOmittedReplyUrison the target,budgetOmittedReplieson a kept reply, andbudgetOmittedParentson the topmost parent kept — fetching those AT-URIs reads everything left out - Surfaces the author's reply gate when set (who may reply) and the AT-URIs of any replies the author hid; deleted posts return
notFound: trueand blocked postsblocked: true - Reply depth renders on the author heading (
### ↳2 Name) rather than by indentation, so a deeply nested reply never crosses into a markdown code block invalid_at_uriandpost_not_founderrors when the AT-URI doesn't resolve; AT-URIs come from theurifield of any returned post- A feed generator AT-URI (
app.bsky.feed.generator) is rejected before any request asuri_is_feed, pointing tobsky_get_feed
bsky_get_post_quotes tool
- Takes a post AT-URI or its bsky.app page; any other collection is rejected before the upstream call
- A DID-authority post costs one request; a handle costs one extra lookup (
resolveHandle), sincegetQuotesanswers a handle authority with an empty list - Every result quotes the same post, so each result's embed keeps only that post's AT-URI and CID, its
recordKindwhen it's unreadable, and any media the quoting post attached — the queried post's text and attachments aren't repeated on every item - Up to 100 quotes per call, newest first, paginated via cursor; truncation is disclosed on the cursor, since pages often come back short of
limitwith more behind them - A page that would pass the 48,000-byte response budget is asked for again at the
limitthat fits, markedbudgetCapped: true, and its cursor resumes at the first quote left out quoteCountis an upper bound on what this returns — Bluesky's counter keeps quotes that have left the indexpost_not_foundwhen the post doesn't exist or its handle doesn't resolve (an empty first page is checked with onegetPosts);invalid_cursorafter one request for a cursor Bluesky can't decode
bsky_search_actors tool
- Returns ranked profiles with handle, DID, displayName, bio, and pronouns when set — no follower, following, or post counts and no
website, which onlybsky_get_profilereturns - Each account carries its two verification statuses (
verification.verifiedStatus,verification.trustedVerifierStatus) when Bluesky sent them, which tells a verified account from a look-alike handle; who issued the verification is onbsky_get_profile - Bio renders as a markdown blockquote in
content[], since it's account-authored text - Up to 100 results per call, paginated via cursor; pages often hold fewer than
limitand still continue, and the last page carries no cursor invalid_cursor, after one request, when Bluesky can't decode the cursor passed (it answers those with HTTP 400)- Use before
bsky_get_profileorbsky_get_author_feedwhen you have a name but not a confirmed handle
bsky_get_follows tool
direction:followers(who follows the actor) orfollowing(who the actor follows)sort:latest(Bluesky's default — most recent follows first) ortop(Bluesky's own ranking of prominent accounts, not a follower-count order); pass a cursor back with the samesort- Takes a handle or DID, also as
@handleor the account's bsky.app page - Returns paginated profiles (handle, DID, displayName, bio, pronouns when set, verification statuses) plus the subject's own profile summary, which carries its statuses too
- No follower, following, or post counts and no
websiteon this view, for the list or the subject — resolve withbsky_get_profilewhen they matter - Up to 100 per page, paginated via cursor. A cursor can lead to an empty page when the remaining accounts no longer resolve, which the response reports as the end of the list
actor_not_foundwhen the handle or DID doesn't resolve
bsky_get_trending tool
- Returns topics with display name, Bluesky's one-sentence story summary (
description), post count, category, status (e.g.hot,cooling,stale), start time, and up to 5 representative accounts driving each topic - Each trend is backed by a feed generator:
feedUriis that feed's AT-URI, parsed from the trend'slink, andbsky_get_feedreads its posts;topicis the feed's record key, not a search term - No cursor — returns the current snapshot up to
limit(default 10, max 25, Bluesky's own maximum);truncatedmeans more topics are trending thanlimit, so it never appears at 25 - Uses
app.bsky.unspecced.getTrends, an unstable endpoint Bluesky may change without notice
bsky://profile/{actor} resource
- Returns the same fields as
bsky_get_profilein injectable-context form — displayName, handle, DID, bio, pronouns, website, follower/following/post counts, avatar, moderation labels, pinned post AT-URI, full verification state - Addressable by handle or DID via
{actor}, with or without a leading@(bsky://profile/@bsky.app) actor_not_foundwhen the handle doesn't resolve
Features
Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
Bluesky-specific:
- Eight of nine tools read
api.bsky.appwithout credentials;bsky_search_postsruns through an optional app-password session and is left out without one - Shared links as input —
@handleand a bsky.app profile URL wherever an account is asked for, a bsky.app post or feed URL wherever that post or feed is, each rewritten to the handle, DID, or AT-URI it names before the request - Single
BlueskyServicewrapping the AT Protocol AppView, with a 15s per-request timeout, retry on transient upstream failures (up to 3 retries, 500ms base delay — never for a login, or for a failure already mapped to a tool error), and a versionedUser-Agent - No HTML in any error — a block page from Bluesky's edge is dropped from the error data, leaving the status
- Embed normalization — raw nested AT Protocol embed objects flattened into a clean
type-discriminated union - Moderation labels surfaced verbatim and unfiltered
- AT Protocol identifier types (handle, DID, AT-URI) explained at first encounter in each tool's description
Agent-friendly output:
- AT-URIs on every post — chain
bsky_get_trending→bsky_get_feed→bsky_get_post_threadwithout extra steps - Verification on every account an agent picks or cites — post authors on all five post tools, actor search, and the follow graph carry Bluesky's
verifiedStatus/trustedVerifierStatuson the author's DID line;bsky_get_profileadds who issued each verification - Discriminated embed union —
type: "images" | "external" | "record" | "video" | "unknown"lets callers branch on data instead of parsing$typestrings; an unmapped lexicon type arrives asunknownwith its raw$typerather than vanishing - Third-party text rendered as markdown blockquotes — post bodies, bios, alt text, and link-card text render as
>-prefixed blockquotes incontent[], so a post's own heading or code fence never merges with the server's structure; values that render inline (display names, pronouns, topic names) have line breaks folded to spaces for the same reason. Inside both, the Markdown and raw HTML a CommonMark renderer would act on — emphasis, links, images, code, lists, headings, tags, entities — is escaped, so user text displays as written; URLs stay copyable andstructuredContentcarries every string unmodified. Each quote ends before the next line the server writes, so a rendering client never folds a post's counts or labels into it - Bounded truncation disclosure — thread and pagination shortfalls (
truncated,unreturnedReplies,parentChainTruncated,hitsTotalat its 10,000 cap) are reported as bounds rather than measurements - A 48,000-byte response budget per surface on every post-returning tool, measured on the rendered output and cut only between whole posts — pages are re-requested at the
limitthat fits so their cursor still continues where they end, threads mark the posts they left out by AT-URI, andbudgetCappedsays when either happened; a response that fits is unchanged
Getting started
Public Hosted Instance
Connect directly — no installation required:
{
"mcpServers": {
"bluesky-mcp-server": {
"type": "streamable-http",
"url": "https://bluesky.caseyjhand.com/mcp"
}
}
}
Self-Hosted / Local
Add the following to your MCP client configuration file. No API key required; add BLUESKY_IDENTIFIER and BLUESKY_APP_PASSWORD to env to enable post search.
{
"mcpServers": {
"bluesky-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/bluesky-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"bluesky-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/bluesky-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"bluesky-mcp-server": {
"type": "stdio",
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "MCP_TRANSPORT_TYPE=stdio",
"ghcr.io/cyanheads/bluesky-mcp-server:latest"
]
}
}
}
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
Prerequisites
- Bun v1.4.0 or higher (or Node.js v24+).
- No API key or account required for anything but post search. See Post search to enable it.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/bluesky-mcp-server.git
- Navigate into the directory:
cd bluesky-mcp-server
- Install dependencies:
bun install
- Configure environment (optional):
cp .env.example .env
# edit .env to override any framework defaults
Configuration
This server requires no API keys. Everything below is optional.
| Variable | Description | Default |
|---|---|---|
BLUESKY_IDENTIFIER | Handle, DID, or email of the account post search runs as. Set together with BLUESKY_APP_PASSWORD, or neither. | — |
BLUESKY_APP_PASSWORD | App password for that account. Without the pair, bsky_search_posts is not offered. | — |
MCP_TRANSPORT_TYPE | Transport: stdio or http | stdio |
MCP_SESSION_MODE | HTTP session mode: stateful, stateless, or auto. Unset or empty, the server resolves to stateless from its own createApp({ sessionMode }) declaration; an explicit value still overrides it. | stateless |
MCP_HTTP_PORT | Port for HTTP server | 3010 |
MCP_AUTH_MODE | Auth mode: none, jwt, or oauth | none |
MCP_LOG_LEVEL | Log level (RFC 5424) | info |
LOGS_DIR | Directory for log files (Node.js only) | <project-root>/logs |
STORAGE_PROVIDER_TYPE | Storage backend | in-memory |
OTEL_ENABLED | Enable OpenTelemetry instrumentation | false |
See .env.example for the full list of optional overrides.
Post search
Bluesky refuses app.bsky.feed.searchPosts without a signed-in account, so bsky_search_posts is registered only when BLUESKY_IDENTIFIER and BLUESKY_APP_PASSWORD are both set. Setting one without the other fails startup with a message naming the missing variable.
Shortened here. Read the whole README on GitHub.
Tools it offers (8)
What this server listed when Ahel dialed its public endpoint in Oct 2026, with no key and no account of yours. The names are the server’s own.
bsky_get_profilebsky_search_actorsbsky_get_trendingbsky_get_feedbsky_get_author_feedbsky_get_post_threadbsky_get_post_quotesbsky_get_follows
Signals
- GitHub stars
- 2
- Last commit
- Sep 2026
- Weekly_downloads
- 653 weekly_downloads
Advanced
- Delivery
- bluesky-mcp-server MCP server → your Ahel connector (mcp.ahel.ai) → your AI.
- Item type
- mcp-server
- Key
io-github-cyanheads-bluesky-mcp-server- Source
- github.com/cyanheads/bluesky-mcp-server
- Hosted endpoint
https://bluesky.caseyjhand.com/mcp
github.com/cyanheads/bluesky-mcp-server