The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Bluesky MCP Server listing page.
Search posts, profiles, feeds, threads, and trending topics on Bluesky via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://bluesky.caseyjhand.com/mcp
Seven tools for read-only access to the public Bluesky/AT Protocol AppView — no authentication required:
| Tool | Description |
|---|---|
bsky_search_posts | Full-text search across public Bluesky posts, with author, language, tag, date, and sort filters |
bsky_get_profile | Fetch a Bluesky actor's public profile by handle or DID — the handle↔DID resolver |
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 — parent chain upward and reply tree downward, with what Bluesky counted but did not return |
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 post count, category, status, and the accounts driving each topic |
bsky_search_postsFull-text search across public Bluesky posts.
since/until), and sort order (top or latest)Status: 400hitsTotal when available, reported as the bound it is — Bluesky caps the count at 10,000, so exactly 10,000 means "at least 10,000" and both the field description and the rendered header say sotruncated, shown, cap, and guidance) in both structuredContent and the content[] trailer when more posts match than came back. A returned cursor alone does not trigger it — Bluesky sends one on every non-empty response, exhausted or not, so hitsTotal is what settles whether the page was cut shortimages (also covers app.bsky.embed.gallery), external (link cards), record (quoted posts), video, unknownembeds, so a quote of an image post is not reduced to a line of text, alongside media for anything the quoting post attached beside the quote. The rendered output names which post each block belongs to, since the two belong to different accountsomittedEmbeds and says so in the rendered text, so it never reads as a quote that had nonerecordKind naming the case, instead of arriving as an empty quotebsky_get_profileFetch a Bluesky actor's public profile by handle or DID.
website is the one link a profile carries in a field of its own rather than inside the bio; both it and pronouns are absent for accounts that set neitherbsky_get_author_feedA user's recent feed ordered newest-first — their own posts and their reposts.
posts_with_replies, posts_no_replies (excludes replies), posts_with_media, or posts_and_author_threads. None of them exclude reposts — the AppView has no repost filterrepostedBy and repostedAt; author always names whoever wrote the postlimit counts reposts too, a page from an account that reposts heavily holds far fewer of that account's own posts than the limit suggests — 30 items from one such account came back as 10 originals and 20 reposts. The response reports originalPosts and reposts whenever a repost is present, so the yield is stated rather than reconstructed by countingbsky_get_post_threadFetch the conversation for a post by AT-URI.
depth (reply tree depth, default 6, max 10 — Bluesky returns no more than 10 levels however deep the request) and parent_height (parent chain height, default 80, max 100)replyCount carries truncated: true with unreturnedReplies and a truncationReason: "depth" (the tree ends there — fetch that node's AT-URI as its own thread to continue) or "unavailable" (no request closes the gap). The response totals the difference for the whole threadparent_height and gives no signal that it did, so the topmost post returned would otherwise be indistinguishable from the start of the conversation. When it is not, that node carries parentChainTruncated: true and the response names its AT-URI — unlike the reply shortfall this one is fully recoverable, since parent_height is honored level for level and fetching that node as its own thread walks further up### ↳2 Name (@handle) — two levels below the top-level reply it descends from) rather than a left margin, so a nested reply stays readable markdown. Indenting instead would push every line below the second level past four spaces, which renders the whole nested half of a thread as a code block. The number is stated rather than repeated as a glyph, since a reply can sit nine levels down and a run of nine arrows has to be counted to be readnotFound: true and posts hidden by a block as blocked: true; both keep the AT-URI Bluesky reportedbsky_search_posts or bsky_get_author_feedbsky_get_followsFetch social graph edges for an account.
direction: followers (who follows the actor) or following (who the actor follows)website — the view these two endpoints return does not carry it; resolve the account with bsky_get_profile when it mattersbsky_get_trendingFetch real-time trending topics on Bluesky.
limitapp.bsky.unspecced.getTrends — Bluesky may change this endpoint without notice| Type | Name | Description |
|---|---|---|
| Resource | 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.
Built on @cyanheads/mcp-ts-core:
none, jwt, oauthin-memory, filesystem, Supabase, Cloudflare KV/R2/D1Bluesky-specific:
api.bsky.app without credentialsBlueskyService wrapping the AT Protocol public AppView with retry (3 attempts, 500ms base), 15s timeout, and a versioned User-Agenttype-discriminated unionAgent-friendly output:
bsky_search_posts → bsky_get_post_thread without extra stepstype: "images" | "external" | "record" | "video" | "unknown") — branch on data, not $type strings; an unmapped lexicon type arrives as unknown with its raw $type rather than vanishingrecordKind — an agent can tell a deleted or blocked quote from one whose text was simply not returnedomittedEmbeds rather than dropped, so the bound on embed recursion is visible in both channelscontent[] emits — image and video URLs, alt text, link-card title and description, the AT-URI and CID of a quoted post, the author's handle, DID, display name, and avatar, and each moderation label with the labeler that applied it and when — so a client reading structuredContent and one reading the rendered text see the same post. Pixel dimensions and a link card's preview thumbnail are left upstream rather than reaching one channel alone, and account-level detail is a bsky_get_profile lookup away> and blank lines kept as a bare >. A post carrying its own ### heading, --- rule, or fenced code block stays inside the quote instead of merging with the server's own section structure, so third-party content never reaches a model in the same channel as the server's labels. Values that render inside a line rather than as a block — display names, pronouns, topic names, moderation label values — have their line breaks folded to spaces for the same reason. structuredContent carries every string unchangedhitsTotal on search results, framed as a lower bound at Bluesky's 10,000 cap — communicate result scale without reporting a ceiling as a measurementtruncated, unreturnedReplies, truncationReason, parentChainTruncated) plus a thread-wide total, stated as a bound rather than a cause — agents can tell how much of a conversation may be missing at either end, which part another request can still reach, and how much of the gap the thread author explainsConnect directly — no installation required:
Add the following to your MCP client configuration file. No API key required.
Or with npx (no Bun required):
Or with Docker:
For Streamable HTTP, set the transport and start the server:
api.bsky.app without credentials.This server requires no API keys. All framework configuration is optional.
| Variable | Description | Default |
|---|---|---|
MCP_TRANSPORT_TYPE | Transport: stdio or http | stdio |
MCP_SESSION_MODE | HTTP session mode: auto, stateful, or stateless. The schema default auto resolves to stateful. | 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.
Build and run:
Run checks and tests:
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/bluesky-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point — registers tools, resource, and inits service. |
src/services/bluesky | AT Protocol AppView HTTP client with retry, timeout, and User-Agent. |
src/mcp-server/tools | Tool definitions (*.tool.ts) — seven read-only Bluesky tools. |
src/mcp-server/resources | Resource definitions (*.resource.ts) — bsky://profile/{actor}. |
tests/ | Unit and integration tests mirroring src/. |
See CLAUDE.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for request-scoped logging, ctx.state for tenant-scoped storagesrc/index.tsIssues and pull requests are welcome. Run checks and tests before submitting:
Apache-2.0 — see LICENSE for details.