Search posts, profiles, feeds, threads, and trending topics on Bluesky.
Search posts, profiles, feeds, threads, and trending topics on Bluesky via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://bluesky.caseyjhand.com/mcp
Public Bluesky data over the AT Protocol AppView — no authentication required. Search posts, resolve profiles, walk feeds and threads, and track trending topics from any MCP client. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
| 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 |
| 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.
bsky_search_posts toolsince/until date range, and top/latest sort; up to 100 results per call via opaque cursor pagination"qqq") returns unfiltered results rather than an errorupstream_rejected_filter error reason instead of a bare status codehitsTotal is capped at 10,000 — a value of exactly 10,000 means "at least that many," not an exact count; truncated/shown/cap disclose when more posts matched than were returned (a cursor alone doesn't imply truncation — Bluesky returns one on every non-empty response)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 postbsky_get_profile toolwebsite is the one link carried in its own field rather than inside the bio; both it and pronouns are absent when the account set neithercontent[], since it's account-authored text that can carry its own markdown structureactor_not_found when the handle doesn't resolve — resolve the handle with bsky_search_actors firstbsky_get_author_feed toolfilter: posts_with_replies, posts_no_replies (default, excludes replies), posts_with_media, or posts_and_author_threads — none exclude reposts, since the AppView has no repost filterrepostedBy and repostedAt; author always names who actually wrote the postlimit counts reposts 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 presentactor_not_found when the handle or DID doesn't resolvebsky_get_post_thread tooldepth (reply levels, default 6, max 10 — Bluesky's own ceiling, however deep the request) and parent_height (parent chain height, default 80, max 100)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)parent_height short of the conversation root, the topmost node carries parentChainTruncated: true — recoverable by fetching that node's AT-URI as its own threadnotFound: true and blocked posts blocked: true### ↳2 Name) rather than by indentation, so a deeply nested reply never crosses into a markdown code blockinvalid_at_uri and post_not_found errors when the AT-URI doesn't resolve; AT-URIs come from bsky_search_posts or bsky_get_author_feedbsky_search_actors toolwebsite, which only bsky_get_profile returnscontent[], since it's account-authored textbsky_get_profile or bsky_get_author_feed when you have a name but not a confirmed handlebsky_get_follows tooldirection: followers (who follows the actor) or following (who the actor follows)website field on this view — resolve with bsky_get_profile when it mattersactor_not_found when the handle or DID doesn't resolvebsky_get_trending toolhot/rising), start time, and up to 5 representative accounts driving each topiclimit (default 10, max 25)app.bsky.unspecced.getTrends, an unstable endpoint Bluesky may change without noticebsky://profile/{actor} resourcebsky_get_profile in injectable-context form — displayName, handle, DID, bio, pronouns, website, follower/following/post counts, avatar, moderation labels, pinned post AT-URI{actor}actor_not_found when the handle doesn't resolveBuilt 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:
api.bsky.app without credentialsBlueskyService wrapping the AT Protocol public AppView, with a 15s per-request timeout, retry (up to 3 retries, 500ms base delay), and a versioned User-Agenttype-discriminated unionAgent-friendly output:
bsky_search_posts → bsky_get_post_thread without extra stepstype: "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>-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 reasontruncated, unreturnedReplies, parentChainTruncated, hitsTotal at its 10,000 cap) are reported as bounds rather than measurementsConnect directly — no installation required:
{
"mcpServers": {
"bluesky-mcp-server": {
"type": "streamable-http",
"url": "https://bluesky.caseyjhand.com/mcp"
}
}
}
Add the following to your MCP client configuration file. No API key required.
{
"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
api.bsky.app without credentials.git clone https://github.com/cyanheads/bluesky-mcp-server.git
cd bluesky-mcp-server
bun install
cp .env.example .env
# edit .env to override any framework defaults
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: 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.
Build and run:
# One-time build
bun run rebuild
# Run the built server
bun run start:stdio
# or
bun run start:http
Run 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 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.
| 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 are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
Apache-2.0 — see LICENSE for details.
Source-derived launch command. Check the maintainer’s required arguments and credentials before running:
npx -y @cyanheads/bluesky-mcp-serverMerge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.
{
"mcpServers": {
"io-github-cyanheads-bluesky-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/bluesky-mcp-server"
]
}
}
}Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.
Claude Desktop setup referenceio.github.cyanheads/bluesky-mcp-server works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.