Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions packages/backend/src/adapters/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,21 @@ import * as attio from './intl/attio.json';
import * as basecamp from './intl/basecamp.json';
import * as beehiiv from './intl/beehiiv.json';
import * as bigcommerce from './intl/bigcommerce.json';
import * as bluesky from './intl/bluesky.json';
import * as brevo from './intl/brevo.json';
import * as bugsnag from './intl/bugsnag.json';
import * as calendly from './intl/calendly.json';
import * as chargebee from './intl/chargebee.json';
import * as clearbit from './intl/clearbit.json';
import * as clickup from './intl/clickup.json';
import * as clockify from './intl/clockify.json';
import * as close from './intl/close.json';
import * as coda from './intl/coda.json';
import * as coingecko from './intl/coingecko.json';
import * as convertkit from './intl/convertkit.json';
import * as copper from './intl/copper.json';
import * as crisp from './intl/crisp.json';
import * as datadog from './intl/datadog.json';
import * as discordBot from './intl/discord-bot.json';
import * as drip from './intl/drip.json';
import * as dropboxSign from './intl/dropbox-sign.json';
Expand All @@ -59,9 +64,13 @@ import * as fathom from './intl/fathom.json';
import * as fillout from './intl/fillout.json';
import * as folk from './intl/folk.json';
import * as freshdesk from './intl/freshdesk.json';
import * as freshservice from './intl/freshservice.json';
import * as front from './intl/front.json';
import * as ghost from './intl/ghost.json';
import * as gitbook from './intl/gitbook.json';
import * as gorgias from './intl/gorgias.json';
import * as hackernews from './intl/hackernews.json';
import * as harvest from './intl/harvest.json';
import * as heap from './intl/heap.json';
import * as height from './intl/height.json';
import * as helpScout from './intl/help-scout.json';
Expand All @@ -78,14 +87,18 @@ import * as magento from './intl/magento.json';
import * as mailchimp from './intl/mailchimp.json';
import * as mailshake from './intl/mailshake.json';
import * as mapbox from './intl/mapbox.json';
import * as mastodon from './intl/mastodon.json';
import * as medium from './intl/medium.json';
import * as messagebird from './intl/messagebird.json';
import * as microsoftBookings from './intl/microsoft-bookings.json';
import * as microsoftTeams from './intl/microsoft-teams.json';
import * as mintlify from './intl/mintlify.json';
import * as mollie from './intl/mollie.json';
import * as neverbounce from './intl/neverbounce.json';
import * as newRelic from './intl/new-relic.json';
import * as newsapi from './intl/newsapi.json';
import * as nominatim from './intl/nominatim.json';
import * as openweather from './intl/openweather.json';
import * as outreach from './intl/outreach.json';
import * as pandadoc from './intl/pandadoc.json';
import * as pipedrive from './intl/pipedrive.json';
Expand All @@ -105,8 +118,10 @@ import * as tally from './intl/tally.json';
import * as telegramBot from './intl/telegram-bot.json';
import * as ticktick from './intl/ticktick.json';
import * as todoist from './intl/todoist.json';
import * as togglTrack from './intl/toggl-track.json';
import * as trello from './intl/trello.json';
import * as typeform from './intl/typeform.json';
import * as uptimeRobot from './intl/uptime-robot.json';
import * as vercelAnalytics from './intl/vercel-analytics.json';
import * as whatsappBusiness from './intl/whatsapp-business.json';
import * as woocommerce from './intl/woocommerce.json';
Expand Down Expand Up @@ -231,16 +246,21 @@ const RAW_ADAPTERS: AdapterDefinition[] = [
basecamp as unknown as AdapterDefinition,
beehiiv as unknown as AdapterDefinition,
bigcommerce as unknown as AdapterDefinition,
bluesky as unknown as AdapterDefinition,
brevo as unknown as AdapterDefinition,
bugsnag as unknown as AdapterDefinition,
calendly as unknown as AdapterDefinition,
chargebee as unknown as AdapterDefinition,
clearbit as unknown as AdapterDefinition,
clickup as unknown as AdapterDefinition,
clockify as unknown as AdapterDefinition,
close as unknown as AdapterDefinition,
coda as unknown as AdapterDefinition,
coingecko as unknown as AdapterDefinition,
convertkit as unknown as AdapterDefinition,
copper as unknown as AdapterDefinition,
crisp as unknown as AdapterDefinition,
datadog as unknown as AdapterDefinition,
discordBot as unknown as AdapterDefinition,
drip as unknown as AdapterDefinition,
dropboxSign as unknown as AdapterDefinition,
Expand All @@ -250,9 +270,13 @@ const RAW_ADAPTERS: AdapterDefinition[] = [
fillout as unknown as AdapterDefinition,
folk as unknown as AdapterDefinition,
freshdesk as unknown as AdapterDefinition,
freshservice as unknown as AdapterDefinition,
front as unknown as AdapterDefinition,
ghost as unknown as AdapterDefinition,
gitbook as unknown as AdapterDefinition,
gorgias as unknown as AdapterDefinition,
hackernews as unknown as AdapterDefinition,
harvest as unknown as AdapterDefinition,
heap as unknown as AdapterDefinition,
height as unknown as AdapterDefinition,
helpScout as unknown as AdapterDefinition,
Expand All @@ -269,14 +293,18 @@ const RAW_ADAPTERS: AdapterDefinition[] = [
mailchimp as unknown as AdapterDefinition,
mailshake as unknown as AdapterDefinition,
mapbox as unknown as AdapterDefinition,
mastodon as unknown as AdapterDefinition,
medium as unknown as AdapterDefinition,
messagebird as unknown as AdapterDefinition,
microsoftBookings as unknown as AdapterDefinition,
microsoftTeams as unknown as AdapterDefinition,
mintlify as unknown as AdapterDefinition,
mollie as unknown as AdapterDefinition,
neverbounce as unknown as AdapterDefinition,
newRelic as unknown as AdapterDefinition,
newsapi as unknown as AdapterDefinition,
nominatim as unknown as AdapterDefinition,
openweather as unknown as AdapterDefinition,
outreach as unknown as AdapterDefinition,
pandadoc as unknown as AdapterDefinition,
pipedrive as unknown as AdapterDefinition,
Expand All @@ -296,8 +324,10 @@ const RAW_ADAPTERS: AdapterDefinition[] = [
telegramBot as unknown as AdapterDefinition,
ticktick as unknown as AdapterDefinition,
todoist as unknown as AdapterDefinition,
togglTrack as unknown as AdapterDefinition,
trello as unknown as AdapterDefinition,
typeform as unknown as AdapterDefinition,
uptimeRobot as unknown as AdapterDefinition,
vercelAnalytics as unknown as AdapterDefinition,
whatsappBusiness as unknown as AdapterDefinition,
woocommerce as unknown as AdapterDefinition,
Expand Down
202 changes: 202 additions & 0 deletions packages/backend/src/adapters/intl/bluesky.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
{
"slug": "bluesky",
"name": "Bluesky",
"description": "Read & post on Bluesky (the AT Protocol social network) from any AI agent. 9 tools, app-password auth.",
"instructions": "This connector wraps the Bluesky AT Protocol XRPC API (bsky.social).\n\n**Setup**:\n1. In the Bluesky app or web client → **Settings → Privacy and Security → App Passwords → Add App Password**.\n2. Name it (e.g. 'mcp') and copy the generated `xxxx-xxxx-xxxx-xxxx` password. App passwords scope down what they can do — never use your account password.\n3. Set `BLUESKY_HANDLE` (e.g. `you.bsky.social`) and `BLUESKY_APP_PASSWORD`.\n\n**Two-step auth**: AT Protocol uses a session model. You can't just send the app password on every call — you must first POST to `/xrpc/com.atproto.server.createSession` with `{identifier, password}` and use the returned `accessJwt` as the Bearer token. The `bluesky_create_session` tool does step 1; subsequent tools use the JWT.\n\nFor production use the access JWT should be cached (it lasts ~2 hours) and refreshed with `refreshJwt` via `/xrpc/com.atproto.server.refreshSession`. This adapter exposes session create + refresh as tools so the caller can manage lifecycle.\n\n**DIDs vs handles**: Bluesky internally uses DIDs (`did:plc:abcd...`). Public APIs accept either DID or handle (`you.bsky.social`); use `bluesky_resolve_handle` to convert.\n\n**Post records**: posts are AT Protocol records of type `app.bsky.feed.post`. They have `text` (max 300 graphemes), `createdAt` (RFC3339), `langs[]`, and optional `embed` for images/links.\n\n**Rate limits**: 5k req / hour per JWT; create-session limited to 30/5min per IP. 429 with `Retry-After`.\n\n**Out of scope here**: image upload multipart (use the public URL embed pattern instead), labelers/moderation, custom feeds CRUD, DM (separate API), the firehose.",
"region": "intl",
"category": "social",
"icon": "bluesky",
"docsUrl": "https://docs.bsky.app/",
"requiredEnvVars": ["BLUESKY_HANDLE", "BLUESKY_APP_PASSWORD"],
"connector": {
"name": "Bluesky AT Protocol",
"type": "REST",
"baseUrl": "https://bsky.social",
"authType": "NONE",
"authConfig": {}
},
"tools": [
{
"name": "bluesky_create_session",
"description": "Exchange handle + app password for an access JWT (lasts ~2h) and refresh JWT. Returns `accessJwt`, `refreshJwt`, `handle`, `did`. Call this once at the start of a session, then pass `accessJwt` to other tools that need it.",
"parameters": {
"type": "object",
"properties": {
"identifier": { "type": "string", "description": "Handle (e.g. you.bsky.social) or email." },
"password": { "type": "string", "description": "App password." }
},
"required": ["identifier", "password"]
},
"endpointMapping": {
"method": "POST",
"path": "/xrpc/com.atproto.server.createSession",
"bodyMapping": { "identifier": "$identifier", "password": "$password" }
}
},
{
"name": "bluesky_refresh_session",
"description": "Refresh an access JWT using the refresh JWT.",
"parameters": {
"type": "object",
"properties": {
"refresh_jwt": { "type": "string", "description": "refreshJwt from create_session." }
},
"required": ["refresh_jwt"]
},
"endpointMapping": {
"method": "POST",
"path": "/xrpc/com.atproto.server.refreshSession",
"headers": { "Authorization": "Bearer ${refresh_jwt}" }
}
},
{
"name": "bluesky_resolve_handle",
"description": "Resolve a Bluesky handle (e.g. `bsky.app`) to its DID. Public, no auth needed.",
"parameters": {
"type": "object",
"properties": {
"handle": { "type": "string", "description": "Handle (no @)." }
},
"required": ["handle"]
},
"endpointMapping": {
"method": "GET",
"path": "/xrpc/com.atproto.identity.resolveHandle",
"queryParams": { "handle": "$handle" }
}
},
{
"name": "bluesky_get_profile",
"description": "Get a user's public profile: handle, did, displayName, description, followers, follows, posts count, avatar URL.",
"parameters": {
"type": "object",
"properties": {
"actor": { "type": "string", "description": "Handle or DID." },
"access_jwt": { "type": "string", "description": "Access JWT from create_session." }
},
"required": ["actor", "access_jwt"]
},
"endpointMapping": {
"method": "GET",
"path": "/xrpc/app.bsky.actor.getProfile",
"queryParams": { "actor": "$actor" },
"headers": { "Authorization": "Bearer ${access_jwt}" }
}
},
{
"name": "bluesky_get_timeline",
"description": "Get the authenticated user's home timeline (chronological feed of follows). Paginated.",
"parameters": {
"type": "object",
"properties": {
"access_jwt": { "type": "string", "description": "Access JWT." },
"limit": { "type": "integer", "description": "Max posts (1-100)." },
"cursor": { "type": "string", "description": "Pagination cursor from previous response." }
},
"required": ["access_jwt"]
},
"endpointMapping": {
"method": "GET",
"path": "/xrpc/app.bsky.feed.getTimeline",
"queryParams": { "limit": "$limit", "cursor": "$cursor" },
"headers": { "Authorization": "Bearer ${access_jwt}" }
}
},
{
"name": "bluesky_get_author_feed",
"description": "Get posts by a specific user (their public feed).",
"parameters": {
"type": "object",
"properties": {
"actor": { "type": "string", "description": "Handle or DID." },
"access_jwt": { "type": "string", "description": "Access JWT." },
"limit": { "type": "integer", "description": "Max posts (1-100)." },
"cursor": { "type": "string", "description": "Pagination cursor." },
"filter": { "type": "string", "description": "'posts_with_replies', 'posts_no_replies', 'posts_with_media', 'posts_and_author_threads'." }
},
"required": ["actor", "access_jwt"]
},
"endpointMapping": {
"method": "GET",
"path": "/xrpc/app.bsky.feed.getAuthorFeed",
"queryParams": { "actor": "$actor", "limit": "$limit", "cursor": "$cursor", "filter": "$filter" },
"headers": { "Authorization": "Bearer ${access_jwt}" }
}
},
{
"name": "bluesky_search_posts",
"description": "Full-text search across public posts.",
"parameters": {
"type": "object",
"properties": {
"q": { "type": "string", "description": "Search query." },
"access_jwt": { "type": "string", "description": "Access JWT." },
"limit": { "type": "integer", "description": "Max results (1-100)." },
"cursor": { "type": "string", "description": "Pagination cursor." },
"lang": { "type": "string", "description": "ISO language filter." },
"sort": { "type": "string", "description": "'top' or 'latest'." }
},
"required": ["q", "access_jwt"]
},
"endpointMapping": {
"method": "GET",
"path": "/xrpc/app.bsky.feed.searchPosts",
"queryParams": { "q": "$q", "limit": "$limit", "cursor": "$cursor", "lang": "$lang", "sort": "$sort" },
"headers": { "Authorization": "Bearer ${access_jwt}" }
}
},
{
"name": "bluesky_create_post",
"description": "Post a new skeet. `text` is required (max 300 graphemes). Returns the post's URI + CID.",
"parameters": {
"type": "object",
"properties": {
"access_jwt": { "type": "string", "description": "Access JWT." },
"repo": { "type": "string", "description": "Your DID (from create_session response)." },
"text": { "type": "string", "description": "Post text, max 300 graphemes." },
"created_at": { "type": "string", "description": "RFC3339 datetime (defaults to now if omitted)." },
"langs": { "type": "array", "description": "ISO 639-1 language codes, e.g. ['en']." }
},
"required": ["access_jwt", "repo", "text", "created_at"]
},
"endpointMapping": {
"method": "POST",
"path": "/xrpc/com.atproto.repo.createRecord",
"bodyMapping": {
"repo": "$repo",
"collection": "app.bsky.feed.post",
"record": {
"$type": "app.bsky.feed.post",
"text": "$text",
"createdAt": "$created_at",
"langs": "$langs"
}
},
"headers": { "Authorization": "Bearer ${access_jwt}" }
}
},
{
"name": "bluesky_delete_post",
"description": "Delete one of your own posts by its rkey (record key, the last segment of the at:// URI).",
"parameters": {
"type": "object",
"properties": {
"access_jwt": { "type": "string", "description": "Access JWT." },
"repo": { "type": "string", "description": "Your DID." },
"rkey": { "type": "string", "description": "Record key — the last segment of the post's at:// URI." }
},
"required": ["access_jwt", "repo", "rkey"]
},
"endpointMapping": {
"method": "POST",
"path": "/xrpc/com.atproto.repo.deleteRecord",
"bodyMapping": {
"repo": "$repo",
"collection": "app.bsky.feed.post",
"rkey": "$rkey"
},
"headers": { "Authorization": "Bearer ${access_jwt}" }
}
}
]
}
16 changes: 16 additions & 0 deletions packages/backend/src/adapters/intl/bluesky.live.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as adapter from './bluesky.json';
const a = adapter as unknown as {
connector: { baseUrl: string; authType: string };
tools: Array<{ name: string; endpointMapping: { path: string } }>;
};
describe('bluesky adapter — static spec conformance', () => {
it('bsky.social base URL', () => expect(a.connector.baseUrl).toBe('https://bsky.social'));
it('uses XRPC path namespace for every tool', () => {
for (const t of a.tools) expect(t.endpointMapping.path).toMatch(/^\/xrpc\//);
});
it('exposes session create + refresh', () => {
const names = a.tools.map((t) => t.name);
expect(names).toContain('bluesky_create_session');
expect(names).toContain('bluesky_refresh_session');
});
});
Loading
Loading