bluesky-mcp-server
Provides tools for searching posts, profiles, feeds, threads, and trending topics on Bluesky via the AT Protocol public AppView, enabling read-only access to Bluesky social data without authentication.
Public Hosted Server: https://bluesky.caseyjhand.com/mcp
Overview
Read Bluesky without an account: profiles, lists and starter packs, 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
Tool | Description |
| 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 |
| Fetch a Bluesky actor's public profile by handle or DID — the handle↔DID resolver |
| Read a feed generator's posts or a list's posts and reposts by AT-URI or bsky.app URL |
| Read list or starter-pack membership, or discover an account's lists and starter packs |
| A user's recent posts ordered newest-first, filterable by post type |
| 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 |
| Read the quote posts behind a post's |
| List the accounts that liked or reposted a post, with optional account creation and separate like timestamps |
| Find Bluesky accounts by name or handle fragment |
| Search public feed catalog names and descriptions, then read a returned feedUri with |
| Paginated social graph edges — who a user follows or who follows them |
| 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 |
| 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, curated groups, 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.
Related MCP server: Bluesky MCP
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 paginationsince/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-31Identifier, 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 requesthitsTotalis 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 matchesA 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 responseEmbeds 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 postVideo embeds retain nonempty
alt, including on quoted posts and media attached alongside a quote; multiline alt renders as literal quoted text, like image altModeration 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'sfeedUriworks as-is, and Discover isat://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hotAlso accepts a list AT-URI (
at://<handle-or-did>/app.bsky.graph.list/<rkey>) orhttps://bsky.app/profile/<handle-or-did>/lists/<rkey>, returning posts and reposts by its membersA handle owner costs one extra lookup (
resolveHandle), a DID owner none; unrelated collections, such as a post AT-URI, are rejected before the upstream callA post the feed pinned to its top carries
pinned: true; a repost carriesrepostedBy/repostedAtUp to 100 posts per call, paginated via cursor; truncation is disclosed on the cursor, since a feed can return fewer than
limitwith more behind itA 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 responsefeed_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)A missing list fails as
list_not_found. The server checks list existence when the first page is empty and Bluesky supplies no nonempty cursorAlways unauthenticated, even when search credentials are set
bsky_get_list tool
Pass
listwith a list or starter-pack AT-URI, ahttps://bsky.app/profile/<actor>/lists/<rkey>URL, or ahttps://bsky.app/starter-pack/<actor>/<rkey>URL. Handle authorities resolve to DIDs before reading the recordReturns list identity, creator, purpose, optional description and
listItemCount, and a page of membership records with account DIDs and handles. Purpose is the upstream string, includingapp.bsky.graph.defs#curatelist,#modlist, or#referencelistStarter-pack metadata remains separate from its backing list: name, creator, optional description and join counts, and supplied feed references. Read the backing list or a recommended generator with
bsky_get_feedIf a pack's backing list is confirmed missing, the pack metadata and list URI remain available with a membership-unavailable notice. Missing membership is not an empty member page
To discover an account's collections, pass
actorinstead oflistand choosekind: listsorkind: starterPacks. Each collection has its own cursor; discovery returns the AppView-visible recordsDefault 25 results per page, capped at 100 through the shared clamping policy. Continue with the same record or actor/collection and a nonempty returned cursor, including after an empty page. An empty or absent cursor ends pagination. Reported member counts are separate from the current page length
list_not_found,starter_pack_not_found, andactor_not_foundidentify confirmed missing targets; malformed or mixed input modes fail before any request. Reads are always keyless
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-URIwebsiteandpronounsare absent when the account set neither; optionallinkscarries ordered profile buttons separately fromwebsite, 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 fieldsOptional
associatedcarries only suppliedlists,feedgens,starterPacks, andlabeler; zero counts,labeler: false, and an empty object stay distinct from absent metadata on both the tool and resourceverificationcarries 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 noneThe bio renders as a markdown blockquote in
content[], since it's account-authored text that can carry its own markdown structureactor_not_foundwhen the handle doesn't resolve — resolve the handle withbsky_search_actorsfirstThe 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 pagefilter: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 matchesfilterReposts carry
repostedByandrepostedAt;authoralways names who actually wrote the postUnder 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 presentUp 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 outactor_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_quotesdepth(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 threadA 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 outSurfaces 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: trueReply depth renders on the author heading (
### ↳2 Name) rather than by indentation, so a deeply nested reply never crosses into a markdown code blockinvalid_at_uriandpost_not_founderrors when the AT-URI doesn't resolve; AT-URIs come from theurifield of any returned postA feed generator AT-URI (
app.bsky.feed.generator) is rejected before any request asuri_is_feed, pointing tobsky_get_feedOther non-post collections are rejected locally as
uri_not_post, naming the collection; use anapp.bsky.feed.postaddress
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 listEvery 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 itemUp to 100 quotes per call;
sort: latest(default) is newest first, andsort: topis descending like count as ordered by Bluesky. The appliedsortis 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 oflimitwith more behind themA 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 outquoteCountis 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_get_post_interactions tool
Takes a post AT-URI or bsky.app post URL and
kind: likesorkind: reposts; resolves handle authorities and returns a DID-formuriReturns actor summaries in provider order, including optional account
createdAt(Joined), labels withval/src/cts, and verification statuses. Likes also carrylikedAt(Liked); reposts carry no interaction timestampDefault 25 accounts, cap 100 through the shared clamping policy. Continue with the same URI, kind, and exact returned cursor;
truncated/shown/capdepend only on cursor presence, even on an empty pagePost
likeCountandrepostCountare upper bounds on retrievable interactions. Exhausted cursors do not prove every counted interaction was returned; populated pages make no extra count lookupEmpty first pages check post existence with
getPosts. A first-page repost HTTP 500 retains normal retries, then checks existence: missing ispost_not_found; an existing post preserves the upstream failure. Empty continuation pages do not check existenceUnresolvable handles fail as
post_not_found. Malformed like cursors fail once asinvalid_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 onlybsky_get_profilereturnsAccount
createdAtrenders as Joined when supplied; moderation labels preservevaland optional labelersrcand application timestampctsEach 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_profileBio renders as a markdown blockquote in
content[], since it's account-authored textUp to 100 results per call, paginated via cursor; pages often hold fewer than
limitand still continue, and the last page carries no cursorinvalid_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_search_feeds tool
Searches the provider’s public feed catalog metadata, not post text; returns
feedUri,displayName, optionaldescription, and creator DID/handle with optional display name, in provider orderAlways keyless, even with post-search credentials configured; pass a returned
feedUridirectly tobsky_get_feedto read its postsqueryis required;limitdefaults to 25 and caps at 100 through the shared clamping policy. Pass the opaquecursorwith the same query; cursor presence alone signals continuation, even on an empty pageUses unstable
app.bsky.unspecced.getPopularFeedGenerators, which may change or disappear;catalog_unavailablerecovers throughbsky_get_trending. This is discovery, not comprehensive enumerationA malformed cursor answered with the catalog's generic HTTP 400
InvalidRequestor 500InternalServerErrorfails after one request asinvalid_cursor; drop it or copy the previous response's cursor unchangedFeed 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) 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 samesortTakes a handle or DID, also as
@handleor the account's bsky.app pageReturns 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
createdAtis retained on listed actors and the subject, rendered as Joined; listed actors' labels retainval, optionalsrc, and optionalctsNo follower, following, or post counts and no
websiteon this view, for the list or the subject — resolve withbsky_get_profilewhen they matterUp to 100 per page, paginated via cursor. An empty page with a cursor remains resumable; an empty continuation page without one reports exhaustion
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 topicEach 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 termNo 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 25Uses
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, ordered links, follower/following/post counts, selectedassociatedcounts and labeler status, avatar, moderation labels, pinned post AT-URI, full verification stateAddressable 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:
Eleven of twelve tools read
api.bsky.appwithout credentials;bsky_search_postsruns through an optional app-password session and is left out without oneShared links as input —
@handleand a bsky.app profile URL wherever an account is asked for, and bsky.app post, feed, list, or starter-pack URLs in the matching reader, each rewritten to the handle, DID, or AT-URI it names before the requestSingle
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-AgentNo 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 unionModeration 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_feedsorbsky_get_trending→bsky_get_feed→bsky_get_post_threadwithout extra stepsVerification 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 verificationDiscriminated 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 vanishingThird-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 itBounded truncation disclosure — thread and pagination shortfalls (
truncated,unreturnedReplies,parentChainTruncated,hitsTotalat its 10,000 cap) are reported as bounds rather than measurementsA 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/mcpPrerequisites
Bun v1.4.4 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.gitNavigate into the directory:
cd bluesky-mcp-serverInstall dependencies:
bun installConfigure environment (optional):
cp .env.example .env
# edit .env to override any framework defaultsConfiguration
This server requires no API keys. Everything below is optional.
Variable | Description | Default |
| Handle, DID, or email of the account post search runs as. Set together with | — |
| App password for that account. Without the pair, | — |
| Read-limit policy: clamp oversized requests to the existing caps and disclose requested/applied values; |
|
| Transport: |
|
| HTTP session mode: |
|
| Port for HTTP server |
|
| HTTP endpoint path. The server adds a leading slash if omitted and rejects query strings, fragments, and router patterns. |
|
| Public HTTP or HTTPS base URL. Use a URL without credentials, a query string, or a fragment. | — |
| Auth mode: |
|
| Log level (RFC 5424) |
|
| Directory for log files (Node.js only). Relative paths resolve against the server package. |
|
| Storage backend |
|
|
|
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
createSessionto 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 assearch_login_limitedwithout a request until theratelimit-resettime 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:
# One-time build bun run rebuild # Run the built server bun run start:stdio # or bun run start:httpRun checks and tests:
bun run devcheck # Lint, format, typecheck, security bun run test # Vitest test suite bun run lint:mcp # Validate MCP definitions against spec
Docker
docker build -t bluesky-mcp-server .
docker run --rm -p 3010:3010 bluesky-mcp-serverThe 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
Directory | Purpose |
|
|
| Read-limit policy and the optional app-password pair that enables post search. |
| AT Protocol HTTP client — public AppView reads, the app-password search session, retry, timeout, and |
| Tool definitions ( |
| Resource definitions ( |
| Unit and integration tests mirroring |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
Handlers throw, framework catches — no
try/catchin tool logicUse
ctx.logfor request-scoped logging,ctx.statefor tenant-scoped storageRegister tools in
src/mcp-server/server-surface.tsand resources insrc/index.tsWrap 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:
bun run devcheck
bun run testLicense
Apache-2.0 — see LICENSE for details.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
This server cannot be deployed
Maintenance
Related MCP Connectors
Bluesky post search, author feeds, profiles & threads
Search bounded public Bluesky keywords, handles, mentions, and hashtags.
Search Hacker News, Bluesky, and Substack from a single MCP interface
Related MCP Servers
- AlicenseNot gradedqualityNot gradedmaintenanceMCP server for Bluesky/AT Protocol enabling LLM clients and agents to authenticate, search, post, like, follow, and manage chat on Bluesky.7 npmMIT
- AlicenseAqualityCmaintenanceMCP server for Bluesky/AT Protocol that enables AI agents to search, post, reply, like, and follow.158 npm1MIT
- FlicenseNot gradedqualityDmaintenanceMCP server providing web search, news search, and X/Twitter search capabilities via HTTP or stdio.-
- AlicenseAqualityCmaintenanceMCP server for managing a Bluesky account, enabling posting, replying, liking, reposting, following, searching, and reading timelines/notifications via natural language.18MIT