Bluesky Mcp Server

io.github.cyanheads由 GitHub 帳號 cyanheads 發布v0.7.0更新於 Nov 3, 2026

搜尋 Bluesky 帖子和資訊流;讀取個人資料、主題串和趨勢。支援 STDIO 或 Streamable HTTP。

AI 翻譯由 AI 模型根據下方的原文描述翻譯,可能有誤。

原文描述

Search Bluesky posts and feeds; read profiles, threads, and trends. STDIO or Streamable HTTP.

已驗證Streamable HTTP可網頁執行網頁搜尋與抓取溝通與協作

概覽

Bluesky 的託管 MCP 服務(bluesky.caseyjhand.com);無需登入;Streamable HTTP;10 個工具。

暫無 AI 概覽:以下資訊來自註冊中心條目和服務本身。

端點
bluesky.caseyjhand.com
最近檢查

安裝

在 SourceWeft 中

  1. 開啟 儀表板中的 Bluesky Mcp Server,將其新增到工作區。
  2. 為需要使用其工具的對話啟用該服務。

Web executable,透過 Streamable HTTP。 遠端服務在工作區中設定後即可從網頁執行環境執行。

交給代理程式安裝

把這段提示詞貼上到 Claude Code、Codex、Cursor 等代理程式,或 SourceWeft 對話中。代理程式會讀取該伺服器的安裝指南,先向你展示發布者、端點和登入方式,經你同意後再把它新增到你的客戶端。金鑰不會寫進任何命令。

閱讀 https://sourceweft.com/mcp/io.github.cyanheads%2Fbluesky-mcp-server/install.md,並按其中說明把這個 MCP 伺服器接入我的客戶端。修改任何設定之前,先告訴我它的發布者、端點和登入方式,等我確認。不要把 API 金鑰寫進命令或我會提交的檔案;問我金鑰應從哪裡取得。

檢視代理程式所依據的安裝指南

其他 MCP 客戶端

把它新增到你客戶端的 mcpServers 設定中。

{
  "mcpServers": {
    "bluesky": {
      "type": "http",
      "url": "https://bluesky.caseyjhand.com/mcp"
    }
  }
}

README

@cyanheads/bluesky-mcp-server

Search Bluesky posts and feeds; read profiles, threads, and trends. STDIO or Streamable HTTP.

11 Tools • 1 Profile Resource • 1 Guide

Public Hosted Server: https://bluesky.caseyjhand.com/mcp


Overview

Read Bluesky without an account: profiles, feed catalog searches, trending topics and the feeds behind them, custom and author feeds, threads, and quote posts. Paste a bsky.app link or @handle anywhere one is asked for, and add an optional app password to unlock full-text post search.

Tools

ToolDescription
bsky_search_postsFull-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_profileFetch a Bluesky actor's public profile by handle or DID — the handle↔DID resolver
bsky_get_feedRead a feed generator's posts — a trend's feed, Discover, or any custom feed — by AT-URI or bsky.app URL
bsky_get_author_feedA user's recent posts ordered newest-first, filterable by post type
bsky_get_post_threadFetch 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_quotesRead the quote posts behind a post's quoteCount, newest first or by like count — where much of the reaction to a post lives
bsky_get_post_interactionsList the accounts that liked or reposted a post, with optional account creation and separate like timestamps
bsky_search_actorsFind Bluesky accounts by name or handle fragment
bsky_search_feedsSearch public feed catalog names and descriptions, then read a returned feedUri with bsky_get_feed
bsky_get_followsPaginated social graph edges — who a user follows or who follows them
bsky_get_trendingReal-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

ResourceDescription
bsky://profile/{actor}A Bluesky actor's public profile, addressable by handle or DID

Use bsky_get_profile for programmatic access or bsky://profile/{actor} to inject profile context directly.

Agent guide

bluesky-guide covers topic research, account attribution, and reactions to a post, including identifier chains and coverage checks. It ships with plugin installs and is readable over MCP at skill://bluesky-guide/SKILL.md without credentials.

Capability reference

bsky_search_posts tool

  • Bluesky refuses post search without a signed-in account, so this tool is registered only when BLUESKY_IDENTIFIER and BLUESKY_APP_PASSWORD are set — without them it is absent from tools/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), or search_refused (Bluesky refused the search)
  • Filters: author and mentioned account (handle or DID, also as @handle or a bsky.app profile URL — mentions matches rich-text mentions only), two-letter language code, hashtag (with or without #), linked domain (bare hostname, leading www. dropped) or exact url (in text links or link cards), since/until, and top/latest sort; up to 100 results per call via opaque cursor pagination
  • since/until compare against each post's sort time — the earlier of createdAt and indexedAt — to the whole second, inclusive; a date alone is 00:00:00 UTC, so until: "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-US filters as en)
  • When Bluesky rejects a parameter, its own explanation is surfaced via the upstream_rejected_filter error reason instead of a bare status code; a cursor it can't decode fails as invalid_cursor, after one request
  • hitsTotal is 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/cap are 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 limit that fits and returned whole — budgetCapped: true, with hitsTotal and 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, with omittedEmbeds counting what went deeper and recordKind naming a quote that's deleted, blocked, detached, or not a post
  • Video embeds retain nonempty alt, including on quoted posts and media attached alongside a quote; multiline alt renders as literal quoted text, like image alt
  • Moderation labels are surfaced as-is, unfiltered

Post tools preserve optional rich-text facets separately from the original text: UTF-8 byte ranges (inclusive byteStart, exclusive byteEnd) and complete link URIs, mention DIDs, and tags. Unrecognized feature variants and additional upstream fields remain visible as literal metadata on both channels. Quoted posts retain their own facets within the existing embed-depth bound. Both response channels include this metadata in the response budget; bsky_get_post_quotes omits the queried target's facets together with its restated text.

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's feedUri works as-is, and Discover is at://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 carries repostedBy/repostedAt
  • Up to 100 posts per call, paginated via cursor; truncation is disclosed on the cursor, since a feed can return fewer than limit with more behind it
  • A page that would pass the 48,000-byte response budget is asked for again at the limit that fits and returned whole, marked budgetCapped: 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 @handle or 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
  • website and pronouns are absent when the account set neither; optional links carries ordered profile buttons separately from website, with each record's URI/CID, complete destination URL, optional title/icon, and moderation labels (val, src?, cts?). An omitted array stays omitted and an explicit empty array stays empty; the profile resource returns the same fields
  • Optional associated carries only supplied lists, feedgens, starterPacks, and labeler; zero counts, labeler: false, and an empty object stay distinct from absent metadata on both the tool and resource
  • verification carries Bluesky's full verification state — verifiedStatus and trustedVerifierStatus (valid, invalid, or none, 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_found when the handle doesn't resolve — resolve the handle with bsky_search_actors first
  • 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 @handle or the account's bsky.app page
  • filter: posts_with_replies, posts_no_replies (default, excludes replies), and posts_and_author_threads include reposts; posts_with_media (the actor's own posts with images or video, no link cards) and posts_with_video return none
  • include_pins: true adds the profile's pinned post, marked pinned: true, first on the first page — in addition to limit, and whether or not it matches filter
  • Reposts carry repostedBy and repostedAt; author always names who actually wrote the post
  • Under the filters that include reposts, limit counts them too, so a heavily-reposting account can return far fewer of its own posts than the limit suggests; originalPosts/reposts report 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 limit that fits (the pin not counted), marked budgetCapped: true, and its cursor resumes at the first post left out
  • actor_not_found when 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) and parent_height (parent chain height, default 80, max 100)
  • A node returning fewer replies than its own replyCount carries truncated: true, unreturnedReplies (an upper bound, not an exact shortfall), and truncationReason ("depth" — fetch that node's AT-URI to continue, or "unavailable" — no request closes the gap)
  • When the parent chain stops at parent_height short of the conversation root, the topmost node carries parentChainTruncated: 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: budgetCapped and budgetOmitted (posts left out) in enrichment, budgetOmittedReplyUris on the target, budgetOmittedReplies on a kept reply, and budgetOmittedParents on 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: true and blocked posts blocked: 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_uri and post_not_found errors when the AT-URI doesn't resolve; AT-URIs come from the uri field of any returned post
  • A feed generator AT-URI (app.bsky.feed.generator) is rejected before any request as uri_is_feed, pointing to bsky_get_feed
  • Other non-post collections are rejected locally as uri_not_post, naming the collection; use an app.bsky.feed.post address

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), since getQuotes answers 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 recordKind when 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; sort: latest (default) is newest first, and sort: top is descending like count as ordered by Bluesky. The applied sort is returned on both surfaces; pass cursors back with the same post and sort. Truncation is disclosed on the cursor, since pages often come back short of limit with more behind them
  • A page that would pass the 48,000-byte response budget is asked for again at the limit that fits, marked budgetCapped: true, and its cursor resumes at the first quote left out
  • quoteCount is an upper bound on what this returns — Bluesky's counter keeps quotes that have left the index
  • post_not_found when the post doesn't exist or its handle doesn't resolve (an empty first page is checked with one getPosts); invalid_cursor after one request for a cursor Bluesky can't decode

bsky_get_post_interactions tool

  • Takes a post AT-URI or bsky.app post URL and kind: likes or kind: reposts; resolves handle authorities and returns a DID-form uri
  • Returns actor summaries in provider order, including optional account createdAt (Joined), labels with val/src/cts, and verification statuses. Likes also carry likedAt (Liked); reposts carry no interaction timestamp
  • Default 25 accounts, cap 100 through the shared clamping policy. Continue with the same URI, kind, and exact returned cursor; truncated/shown/cap depend only on cursor presence, even on an empty page
  • Post likeCount and repostCount are upper bounds on retrievable interactions. Exhausted cursors do not prove every counted interaction was returned; populated pages make no extra count lookup
  • Empty first pages check post existence with getPosts. A first-page repost HTTP 500 retains normal retries, then checks existence: missing is post_not_found; an existing post preserves the upstream failure. Empty continuation pages do not check existence
  • Unresolvable handles fail as post_not_found. Malformed like cursors fail once as invalid_cursor; repost cursors can be ignored upstream and repeat a page. Resolution, retries, and existence reads share one 15-second deadline and caller cancellation

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 only bsky_get_profile returns
  • Account createdAt renders as Joined when supplied; moderation labels preserve val and optional labeler src and application timestamp cts
  • 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 on bsky_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 limit and 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_profile or bsky_get_author_feed when you have a name but not a confirmed handle

bsky_search_feeds tool

  • Searches the provider’s public feed catalog metadata, not post text; returns feedUri, displayName, optional description, and creator DID/handle with optional display name, in provider order
  • Always keyless, even with post-search credentials configured; pass a returned feedUri directly to bsky_get_feed to read its posts
  • query is required; limit defaults to 25 and caps at 100 through the shared clamping policy. Pass the opaque cursor with the same query; cursor presence alone signals continuation, even on an empty page
  • Uses unstable app.bsky.unspecced.getPopularFeedGenerators, which may change or disappear; catalog_unavailable recovers through bsky_get_trending. This is discovery, not comprehensive enumeration
  • A malformed cursor answered with the catalog's generic HTTP 400 InvalidRequest or 500 InternalServerError fails after one request as invalid_cursor; drop it or copy the previous response's cursor unchanged
  • Feed names, descriptions, and creator identity render as quoted third-party data; supplied blank values and omitted fields stay distinct

bsky_get_follows tool

  • direction: followers (who follows the actor) or following (who the actor follows)
  • sort: latest (Bluesky's default — most recent follows first) or top (Bluesky's own ranking of prominent accounts, not a follower-count order); pass a cursor back with the same sort
  • Takes a handle or DID, also as @handle or 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
  • Optional account createdAt is retained on listed actors and the subject, rendered as Joined; listed actors' labels retain val, optional src, and optional cts
  • No follower, following, or post counts and no website on this view, for the list or the subject — resolve with bsky_get_profile when 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_found when 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: feedUri is that feed's AT-URI, parsed from the trend's link, and bsky_get_feed reads its posts; topic is 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); truncated means more topics are trending than limit, 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_profile in injectable-context form — displayName, handle, DID, bio, pronouns, website, ordered links, follower/following/post counts, selected associated counts and labeler status, 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_found when 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:

  • Ten of eleven tools read api.bsky.app without credentials; bsky_search_posts runs through an optional app-password session and is left out without one
  • Shared links as input — @handle and 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 BlueskyService wrapping 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 versioned User-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_search_feeds or bsky_get_trending → bsky_get_feed → bsky_get_post_thread without 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 / trustedVerifierStatus on the author's DID line; bsky_get_profile adds who issued each verification
  • Discriminated embed union — type: "images" | "external" | "record" | "video" | "unknown" lets callers branch on data instead of parsing $type strings; an unmapped lexicon type arrives as unknown with its raw $type rather than vanishing
  • Third-party text rendered as markdown blockquotes — post bodies, bios, alt text, and link-card text render as >-prefixed blockquotes in content[], 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 and structuredContent carries 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, hitsTotal at 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 limit that fits so their cursor still continues where they end, threads mark the posts they left out by AT-URI, and budgetCapped says when either happened; a response that fits is unchanged

Getting started

Public Hosted Instance

Connect directly — no installation required:

json
{  "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.

json
{  "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):

json
{  "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:

json
{  "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:

sh
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

  1. Clone the repository:
sh
git clone https://github.com/cyanheads/bluesky-mcp-server.git
  1. Navigate into the directory:
sh
cd bluesky-mcp-server
  1. Install dependencies:
sh
bun install
  1. Configure environment (optional):
sh
cp .env.example .env# edit .env to override any framework defaults

Configuration

This server requires no API keys. Everything below is optional.

VariableDescriptionDefault
BLUESKY_IDENTIFIERHandle, DID, or email of the account post search runs as. Set together with BLUESKY_APP_PASSWORD, or neither.—
BLUESKY_APP_PASSWORDApp password for that account. Without the pair, bsky_search_posts is not offered.—
BLUESKY_CLAMP_READ_LIMITSRead-limit policy: clamp oversized requests to the existing caps and disclose requested/applied values; false restores rejection.true
MCP_TRANSPORT_TYPETransport: stdio or httpstdio
MCP_SESSION_MODEHTTP 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_PORTPort for HTTP server3010
MCP_AUTH_MODEAuth mode: none, jwt, or oauthnone
MCP_LOG_LEVELLog level (RFC 5424)info
LOGS_DIRDirectory for log files (Node.js only)<project-root>/logs
STORAGE_PROVIDER_TYPEStorage backendin-memory
OTEL_ENABLEDEnable OpenTelemetry instrumentationfalse

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.

  • Use a dedicated account with a standard (non-privileged) app password — create one under Settings → Privacy and security → App passwords. The server never needs DM access.
  • Search runs as that account for every caller. The AppView leaves out posts from accounts in a block relationship with it, in either direction; it does not apply mutes. On a shared instance, anyone can block the account and drop their posts from its results.
  • Logins are scarce. Bluesky limits createSession to about 10 per day per account, so the server logs in on the first search — never at startup — reuses the session, refreshes it when the access token expires, and logs in again only when the refresh is rejected. A rejected login is not retried until the process restarts. When Bluesky answers a login or refresh with 429, every search fails as search_login_limited without a request until the ratelimit-reset time it names (15 minutes when it names none), then logs in again.
  • Credentials and tokens stay in memory; they never appear in logs, errors, or tool output, and no other tool sends them.

Running the server

Local development

  • Build and run:

    sh
    # One-time buildbun run rebuild
    # Run the built serverbun run start:stdio# orbun run start:http
  • Run checks and tests:

    sh
    bun run devcheck   # Lint, format, typecheck, securitybun run test       # Vitest test suitebun run lint:mcp   # Validate MCP definitions against spec

Docker

sh
docker build -t bluesky-mcp-server .docker run --rm -p 3010:3010 bluesky-mcp-server

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.

Project structure

DirectoryPurpose
src/index.tscreateApp() entry point — reads config, registers tools and resource, and inits the service.
src/configRead-limit policy and the optional app-password pair that enables post search.
src/services/blueskyAT Protocol HTTP client — public AppView reads, the app-password search session, retry, timeout, and User-Agent.
src/mcp-server/toolsTool definitions (*.tool.ts) — eleven read-only Bluesky tools.
src/mcp-server/resourcesResource definitions (*.resource.ts) — bsky://profile/{actor}.
tests/Unit and integration tests mirroring src/.

Development guide

See CLAUDE.md for development guidelines and architectural rules. The short version:

  • Handlers throw, framework catches — no try/catch in tool logic
  • Use ctx.log for request-scoped logging, ctx.state for tenant-scoped storage
  • Register tools in src/mcp-server/server-surface.ts and resources in src/index.ts
  • Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields

Contributing

Issues are welcome. Run checks and tests before submitting:

sh
bun run devcheckbun run test

License

Apache-2.0 — see LICENSE for details.

來源:README.md,提交 d253fd3

工具

10

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, ordered profile links, follower/following/post counts, associated list/feed/starter-pack counts and labeler status when supplied, 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.

輸入參數actor必填

bsky_search_actors

Search Bluesky Actors

Find Bluesky accounts by name or handle fragment. Returns ranked profiles with handle, DID, displayName, bio, account creation timestamp, moderation labels with provenance, 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.

輸入參數limitquery必填cursor

bsky_search_feeds

Search Bluesky Feeds

Search metadata in the provider’s public feed catalog, without credentials. This searches feed names and descriptions, not post text, and does not enumerate every feed. Returns feeds in provider order with generator AT-URIs and creator identities; pass a feedUri to bsky_get_feed to read its posts. Uses app.bsky.unspecced.getPopularFeedGenerators, an unstable upstream API that may change or become unavailable. Continue with the returned opaque cursor and the same query.

輸入參數limitquery必填cursor

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 bsky_search_feeds catalog results, 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.

輸入參數feed必填limitcursor

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.

輸入參數actor必填limitcursorfilterinclude_pins

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: "budget

輸入參數uri必填depthparent_height

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. Sort latest (default) is newest first; top is descending like count as ordered by Bluesky. 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.

輸入參數uri必填sortlimitcursor

bsky_get_post_interactions

Get Bluesky Post Interactions

List the accounts behind a post’s likes or reposts without credentials, in provider order. Accepts a post AT-URI or bsky.app post URL; a handle authority costs one extra resolution request. Returns actor summaries with account creation and label provenance when supplied, and a separate likedAt timestamp for likes. Account counts and website are available through bsky_get_profile. Post likeCount and repostCount are upper bounds on retrievable interactions: cursor exhaustion does not prove every counted interaction was returned. Follow returned cursors with the same post and kind; no pages are walked automatically. Repost cursors retain Bluesky’s behavior, which can ignore malformed cursors and repeat a page.

輸入參數uri必填kind必填limitcursor

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, account creation timestamp, moderation labels with provenance, 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.

輸入參數sortactor必填limitcursordirection必填

版本歷史

7
  1. v0.7.0最新Nov 3, 2026
  2. v0.6.0Oct 23, 2026
  3. v0.5.2Oct 8, 2026
  4. v0.5.1Sep 25, 2026
  5. v0.5.0Sep 25, 2026
  6. v0.3.2Sep 20, 2026
  7. v0.3.1Sep 16, 2026

評分

已滿足適用檢查項的 100%

該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。

  • 近 30 天內可連通必需25 分
  • 已宣告認證方式必需10 分
  • 已列出工具必需15 分
  • 描述必需15 分
  • 圖示10 分
  • 文件15 分
  • 授權條款10 分