# Directory Structure
```
├── .changeset
│ ├── config.json
│ └── README.md
├── .github
│ └── workflows
│ ├── pkg-pr-new.yml
│ ├── release.yml
│ └── validate.yml
├── .gitignore
├── .node-version
├── .vscode
│ ├── extensions.json
│ └── settings.json
├── biome.json
├── build.config.ts
├── CHANGELOG.md
├── knip.json
├── LICENSE
├── package.json
├── pnpm-lock.yaml
├── README.md
├── src
│ ├── index.ts
│ ├── meta.ts
│ └── tools
│ ├── delete-follow.ts
│ ├── delete-like.ts
│ ├── delete-post.ts
│ ├── delete-repost.ts
│ ├── follow.ts
│ ├── get-followers.ts
│ ├── get-follows.ts
│ ├── get-likes.ts
│ ├── get-post-thread.ts
│ ├── get-profile.ts
│ ├── get-timeline.ts
│ ├── index.ts
│ ├── like.ts
│ ├── post.ts
│ ├── repost.ts
│ └── search-posts.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.node-version:
--------------------------------------------------------------------------------
```
v22.14.0
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
node_modules/
dist/
```
--------------------------------------------------------------------------------
/.changeset/README.md:
--------------------------------------------------------------------------------
```markdown
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# mcp-server-bluesky
MCP server for [Bluesky](https://bsky.app/).
## Usage with Claude Desktop
```json
{
"mcpServers": {
"bluesky": {
"command": "npx",
"args": ["-y", "mcp-server-bluesky"],
"env": {
"BLUESKY_USERNAME": "username",
"BLUESKY_PASSWORD": "password",
"BLUESKY_PDS_URL": "https://bsky.social"
}
}
}
}
```
The `BLUESKY_PDS_URL` is optional and defaults to `https://bsky.social` if not specified.
## Tools
- `bluesky_get_profile`
- `bluesky_follow`
- `bluesky_delete_follow`
- `bluesky_get_follows`
- `bluesky_get_followers`
- `bluesky_search_posts`
- `bluesky_post`
- `bluesky_delete_post`
- `bluesky_repost`
- `bluesky_delete_repost`
- `bluesky_get_timeline`
- `bluesky_get_post_thread`
- `bluesky_get_likes`
- `bluesky_like`
- `bluesky_delete_like`
```
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
```json
{
"recommendations": ["biomejs.biome"]
}
```
--------------------------------------------------------------------------------
/src/meta.ts:
--------------------------------------------------------------------------------
```typescript
export { version } from "../package.json";
```
--------------------------------------------------------------------------------
/knip.json:
--------------------------------------------------------------------------------
```json
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"entry": ["src/index.ts"],
"project": ["src/**/*.ts"],
"ignoreDependencies": ["@changesets/cli"]
}
```
--------------------------------------------------------------------------------
/build.config.ts:
--------------------------------------------------------------------------------
```typescript
import { defineBuildConfig } from "unbuild";
export default defineBuildConfig({
entries: ["src/index"],
clean: true,
rollup: {
emitCJS: false,
esbuild: {
minify: true,
},
},
});
```
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
```json
{
"$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
"changelog": [
"@changesets/changelog-github",
{ "repo": "morinokami/mcp-server-bluesky" }
],
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"rootDir": "./src",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
```
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
```json
{
"editor.codeActionsOnSave": {
"quickfix.biome": "explicit",
"source.organizeImports.biome": "explicit"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[jsonc]": {
"editor.defaultFormatter": "biomejs.biome"
}
}
```
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
```json
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"ignore": []
},
"formatter": {
"enabled": true,
"indentStyle": "tab"
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
}
}
```
--------------------------------------------------------------------------------
/.github/workflows/pkg-pr-new.yml:
--------------------------------------------------------------------------------
```yaml
name: Publish Pull Request
on:
pull_request:
branches: ["main"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: ./.node-version
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Publish
run: pnpm pkg-pr-new publish --bin
```
--------------------------------------------------------------------------------
/src/tools/post.ts:
--------------------------------------------------------------------------------
```typescript
import type { AtpAgent } from "@atproto/api";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const PostArgumentsSchema = z.object({
text: z.string().min(1).max(300),
});
export const postTool: Tool = {
name: "bluesky_post",
description: "Post a message",
inputSchema: {
type: "object",
properties: {
text: {
type: "string",
description: "The text of the message",
},
},
required: ["text"],
},
};
export async function handlePost(
agent: AtpAgent,
args?: Record<string, unknown>,
) {
const { text } = PostArgumentsSchema.parse(args);
const response = await agent.post({ text });
return {
content: [{ type: "text", text: JSON.stringify(response) }],
};
}
```
--------------------------------------------------------------------------------
/src/tools/delete-like.ts:
--------------------------------------------------------------------------------
```typescript
import type { AtpAgent } from "@atproto/api";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const DeleteLikeArgumentsSchema = z.object({
likeUri: z.string(),
});
export const deleteLikeTool: Tool = {
name: "bluesky_delete_like",
description: "Delete a like",
inputSchema: {
type: "object",
properties: {
likeUri: {
type: "string",
description: "The URI of the like to delete",
},
},
required: ["likeUri"],
},
};
export async function handleDeleteLike(
agent: AtpAgent,
args?: Record<string, unknown>,
) {
const { likeUri } = DeleteLikeArgumentsSchema.parse(args);
await agent.deleteLike(likeUri);
return {
content: [{ type: "text", text: "Successfully deleted the like" }],
};
}
```
--------------------------------------------------------------------------------
/src/tools/delete-post.ts:
--------------------------------------------------------------------------------
```typescript
import type { AtpAgent } from "@atproto/api";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const DeletePostArgumentsSchema = z.object({
postUri: z.string(),
});
export const deletePostTool: Tool = {
name: "bluesky_delete_post",
description: "Delete a post",
inputSchema: {
type: "object",
properties: {
postUri: {
type: "string",
description: "The URI of the post to delete",
},
},
required: ["postUri"],
},
};
export async function handleDeletePost(
agent: AtpAgent,
args?: Record<string, unknown>,
) {
const { postUri } = DeletePostArgumentsSchema.parse(args);
await agent.deletePost(postUri);
return {
content: [{ type: "text", text: "Successfully deleted the post" }],
};
}
```
--------------------------------------------------------------------------------
/src/tools/follow.ts:
--------------------------------------------------------------------------------
```typescript
import type { AtpAgent } from "@atproto/api";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const FollowArgumentsSchema = z.object({
subjectDid: z.string(),
});
export const followTool: Tool = {
name: "bluesky_follow",
description: "Follow a user",
inputSchema: {
type: "object",
properties: {
subjectDid: {
type: "string",
description: "The DID of the user to follow",
},
},
required: ["subjectDid"],
},
};
export async function handleFollow(
agent: AtpAgent,
args?: Record<string, unknown>,
) {
const { subjectDid } = FollowArgumentsSchema.parse(args);
const response = await agent.follow(subjectDid);
return {
content: [{ type: "text", text: JSON.stringify(response) }],
};
}
```
--------------------------------------------------------------------------------
/src/tools/delete-follow.ts:
--------------------------------------------------------------------------------
```typescript
import type { AtpAgent } from "@atproto/api";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const FollowArgumentsSchema = z.object({
followUri: z.string(),
});
export const deleteFollowTool: Tool = {
name: "bluesky_delete_follow",
description: "Unfollow a user",
inputSchema: {
type: "object",
properties: {
followUri: {
type: "string",
description: "The URI of the follow record to delete",
},
},
required: ["followUri"],
},
};
export async function handleDeleteFollow(
agent: AtpAgent,
args?: Record<string, unknown>,
) {
const { followUri } = FollowArgumentsSchema.parse(args);
await agent.deleteFollow(followUri);
return {
content: [{ type: "text", text: "Successfully deleted the follow" }],
};
}
```
--------------------------------------------------------------------------------
/src/tools/delete-repost.ts:
--------------------------------------------------------------------------------
```typescript
import type { AtpAgent } from "@atproto/api";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const DeleteRepostArgumentsSchema = z.object({
repostUri: z.string(),
});
export const deleteRepostTool: Tool = {
name: "bluesky_delete_repost",
description: "Delete a repost",
inputSchema: {
type: "object",
properties: {
repostUri: {
type: "string",
description: "The URI of the repost to delete",
},
},
required: ["repostUri"],
},
};
export async function handleDeleteRepost(
agent: AtpAgent,
args?: Record<string, unknown>,
) {
const { repostUri } = DeleteRepostArgumentsSchema.parse(args);
await agent.deleteRepost(repostUri);
return {
content: [{ type: "text", text: "Successfully deleted the repost" }],
};
}
```
--------------------------------------------------------------------------------
/src/tools/get-profile.ts:
--------------------------------------------------------------------------------
```typescript
import type { AtpAgent } from "@atproto/api";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const GetProfileArgumentsSchema = z.object({
actor: z.string().min(1),
});
export const getProfileTool: Tool = {
name: "bluesky_get_profile",
description: "Get a user's profile",
inputSchema: {
type: "object",
properties: {
actor: {
type: "string",
description:
"The DID (or handle) of the user whose profile you'd like to fetch",
},
},
required: ["actor"],
},
};
export async function handleGetProfile(
agent: AtpAgent,
args?: Record<string, unknown>,
) {
const { actor } = GetProfileArgumentsSchema.parse(args);
const response = await agent.getProfile({ actor });
return {
content: [{ type: "text", text: JSON.stringify(response) }],
};
}
```
--------------------------------------------------------------------------------
/src/tools/like.ts:
--------------------------------------------------------------------------------
```typescript
import type { AtpAgent } from "@atproto/api";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const LikeArgumentsSchema = z.object({
uri: z.string(),
cid: z.string(),
});
export const likeTool: Tool = {
name: "bluesky_like",
description: "Like a post",
inputSchema: {
type: "object",
properties: {
uri: {
type: "string",
description: "The URI of the post to like",
},
cid: {
type: "string",
description: "The CID of the post to like",
},
},
required: ["uri", "cid"],
},
};
export async function handleLike(
agent: AtpAgent,
args?: Record<string, unknown>,
) {
const { uri, cid } = LikeArgumentsSchema.parse(args);
const response = await agent.like(uri, cid);
return {
content: [{ type: "text", text: JSON.stringify(response) }],
};
}
```
--------------------------------------------------------------------------------
/src/tools/repost.ts:
--------------------------------------------------------------------------------
```typescript
import type { AtpAgent } from "@atproto/api";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const RepostArgumentsSchema = z.object({
uri: z.string(),
cid: z.string(),
});
export const repostTool: Tool = {
name: "bluesky_repost",
description: "Repost a post",
inputSchema: {
type: "object",
properties: {
uri: {
type: "string",
description: "The URI of the post to repost",
},
cid: {
type: "string",
description: "The CID of the post to repost",
},
},
required: ["uri", "cid"],
},
};
export async function handleRepost(
agent: AtpAgent,
args?: Record<string, unknown>,
) {
const { uri, cid } = RepostArgumentsSchema.parse(args);
const response = await agent.repost(uri, cid);
return {
content: [{ type: "text", text: JSON.stringify(response) }],
};
}
```
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
```yaml
name: Release
on:
push:
branches: ["main"]
jobs:
release:
name: Release
if: ${{ github.repository_owner == 'morinokami' }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: ./.node-version
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Create release pull request
uses: changesets/action@v1
with:
version: pnpm changeset version
publish: pnpm changeset publish
commit: "[ci] release"
title: "[ci] release"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
```
--------------------------------------------------------------------------------
/.github/workflows/validate.yml:
--------------------------------------------------------------------------------
```yaml
name: Validate
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
validate:
runs-on: ubuntu-latest
timeout-minutes: 10
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: ./.node-version
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run Biome check
run: pnpm check
- name: Run typecheck
run: pnpm typecheck
- name: Run Knip
run: pnpm knip
- name: Build
run: pnpm build
- name: Run publint
run: pnpm publint
```
--------------------------------------------------------------------------------
/src/tools/search-posts.ts:
--------------------------------------------------------------------------------
```typescript
import type { AtpAgent } from "@atproto/api";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const SearchPostsArgumentsSchema = z.object({
q: z.string(),
limit: z.number().max(100).optional(),
cursor: z.string().optional(),
});
export const searchPostsTool: Tool = {
name: "bluesky_search_posts",
description: "Search posts",
inputSchema: {
type: "object",
properties: {
q: {
type: "string",
description: "The search query",
},
limit: {
type: "number",
description: "The maximum number of posts to fetch",
},
cursor: {
type: "string",
description: "The cursor to use for pagination",
},
},
required: ["q"],
},
};
export async function handleSearchPosts(
agent: AtpAgent,
args?: Record<string, unknown>,
) {
const { q, limit, cursor } = SearchPostsArgumentsSchema.parse(args);
const response = await agent.app.bsky.feed.searchPosts({ q, limit, cursor });
return {
content: [{ type: "text", text: JSON.stringify(response) }],
};
}
```
--------------------------------------------------------------------------------
/src/tools/get-timeline.ts:
--------------------------------------------------------------------------------
```typescript
import type { AtpAgent } from "@atproto/api";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const GetTimelineArgumentsSchema = z.object({
algorithm: z.string().optional(),
limit: z.number().max(100).optional(),
cursor: z.string().optional(),
});
export const getTimelineTool: Tool = {
name: "bluesky_get_timeline",
description: "Get user's timeline",
inputSchema: {
type: "object",
properties: {
algorithm: {
type: "string",
description: "The algorithm to use for timeline generation",
},
limit: {
type: "number",
description: "The maximum number of posts to fetch",
},
cursor: {
type: "string",
description: "The cursor to use for pagination",
},
},
},
};
export async function handleGetTimeline(
agent: AtpAgent,
args?: Record<string, unknown>,
) {
const { algorithm, limit, cursor } = GetTimelineArgumentsSchema.parse(args);
const response = await agent.getTimeline({ algorithm, limit, cursor });
return {
content: [{ type: "text", text: JSON.stringify(response) }],
};
}
```
--------------------------------------------------------------------------------
/src/tools/get-post-thread.ts:
--------------------------------------------------------------------------------
```typescript
import type { AtpAgent } from "@atproto/api";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const GetPostThreadArgumentsSchema = z.object({
uri: z.string(),
depth: z.number().optional(),
parentHeight: z.number().optional(),
});
export const getPostThreadTool: Tool = {
name: "bluesky_get_post_thread",
description: "Get a post thread",
inputSchema: {
type: "object",
properties: {
uri: {
type: "string",
description: "The URI of the post to get the thread for",
},
depth: {
type: "number",
description: "The levels of reply depth to fetch",
},
parentHeight: {
type: "number",
description: "The number of parent posts to include",
},
},
required: ["uri"],
},
};
export async function handleGetPostThread(
agent: AtpAgent,
args?: Record<string, unknown>,
) {
const { uri, depth, parentHeight } = GetPostThreadArgumentsSchema.parse(args);
const response = await agent.getPostThread({ uri, depth, parentHeight });
return {
content: [{ type: "text", text: JSON.stringify(response) }],
};
}
```
--------------------------------------------------------------------------------
/src/tools/get-follows.ts:
--------------------------------------------------------------------------------
```typescript
import type { AtpAgent } from "@atproto/api";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const GetFollowsArgumentsSchema = z.object({
actor: z.string().min(1),
limit: z.number().max(100).optional(),
cursor: z.string().optional(),
});
export const getFollowsTool: Tool = {
name: "bluesky_get_follows",
description: "Get user's follows",
inputSchema: {
type: "object",
properties: {
actor: {
type: "string",
description:
"The DID (or handle) of the user whose follow information you'd like to fetch",
},
limit: {
type: "number",
description: "The maximum number of follows to fetch",
},
cursor: {
type: "string",
description: "The cursor to use for pagination",
},
},
required: ["actor"],
},
};
export async function handleGetFollows(
agent: AtpAgent,
args?: Record<string, unknown>,
) {
const { actor, limit, cursor } = GetFollowsArgumentsSchema.parse(args);
const response = await agent.getFollows({ actor, limit, cursor });
return {
content: [{ type: "text", text: JSON.stringify(response) }],
};
}
```
--------------------------------------------------------------------------------
/src/tools/get-followers.ts:
--------------------------------------------------------------------------------
```typescript
import type { AtpAgent } from "@atproto/api";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const GetFollowersArgumentsSchema = z.object({
actor: z.string().min(1),
limit: z.number().max(100).optional(),
cursor: z.string().optional(),
});
export const getFollowersTool: Tool = {
name: "bluesky_get_followers",
description: "Get user's followers",
inputSchema: {
type: "object",
properties: {
actor: {
type: "string",
description:
"The DID (or handle) of the user whose followers you'd like to fetch",
},
limit: {
type: "number",
description: "The maximum number of followers to fetch",
},
cursor: {
type: "string",
description: "The cursor to use for pagination",
},
},
required: ["actor"],
},
};
export async function handleGetFollowers(
agent: AtpAgent,
args?: Record<string, unknown>,
) {
const { actor, limit, cursor } = GetFollowersArgumentsSchema.parse(args);
const response = await agent.getFollowers({ actor, limit, cursor });
return {
content: [{ type: "text", text: JSON.stringify(response) }],
};
}
```
--------------------------------------------------------------------------------
/src/tools/get-likes.ts:
--------------------------------------------------------------------------------
```typescript
import type { AtpAgent } from "@atproto/api";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const GetLikesArgumentsSchema = z.object({
uri: z.string(),
cid: z.string().optional(),
limit: z.number().max(100).optional(),
cursor: z.string().optional(),
});
export const getLikesTool: Tool = {
name: "bluesky_get_likes",
description: "Get likes for a post",
inputSchema: {
type: "object",
properties: {
uri: {
type: "string",
description: "The URI of the post to get likes for",
},
cid: {
type: "string",
description: "The CID of the post to get likes for",
},
limit: {
type: "number",
description: "The maximum number of likes to fetch",
},
cursor: {
type: "string",
description: "The cursor to use for pagination",
},
},
required: ["uri"],
},
};
export async function handleGetLikes(
agent: AtpAgent,
args?: Record<string, unknown>,
) {
const { uri, cid, limit, cursor } = GetLikesArgumentsSchema.parse(args);
const response = await agent.getLikes({ uri, cid, limit, cursor });
return {
content: [{ type: "text", text: JSON.stringify(response) }],
};
}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "mcp-server-bluesky",
"description": "MCP server for interacting with Bluesky",
"version": "0.4.1",
"type": "module",
"author": "Shinya Fujino <[email protected]> (https://github.com/morinokami)",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/morinokami/mcp-server-bluesky.git"
},
"bugs": "https://github.com/morinokami/mcp-server-bluesky/issues",
"keywords": [
"modelcontextprotocol",
"mcp",
"bluesky"
],
"packageManager": "[email protected]",
"bin": {
"mcp-server-bluesky": "dist/index.mjs"
},
"files": [
"dist"
],
"scripts": {
"build": "unbuild",
"inspect": "pnpm run build && mcp-inspector node dist/index.mjs",
"check": "biome check src",
"typecheck": "tsc --noEmit",
"publint": "publint",
"knip": "knip"
},
"dependencies": {
"@atproto/api": "0.14.20",
"@modelcontextprotocol/sdk": "1.9.0",
"zod": "3.24.2"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@changesets/changelog-github": "0.5.1",
"@changesets/cli": "2.28.1",
"@modelcontextprotocol/inspector": "0.8.1",
"@types/node": "22.14.0",
"knip": "5.47.0",
"pkg-pr-new": "0.0.42",
"publint": "0.3.10",
"typescript": "5.8.3",
"unbuild": "3.5.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"@biomejs/biome",
"esbuild"
]
}
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { AtpAgent } from "@atproto/api";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { version } from "./meta.js";
import { handleToolCall, tools } from "./tools/index.js";
async function main() {
const identifier = process.env.BLUESKY_USERNAME;
const password = process.env.BLUESKY_PASSWORD;
const service = process.env.BLUESKY_PDS_URL || "https://bsky.social";
if (!identifier || !password) {
console.error(
"Please set BLUESKY_USERNAME and BLUESKY_PASSWORD environment variables",
);
process.exit(1);
}
const agent = new AtpAgent({ service });
const loginResponse = await agent.login({
identifier,
password,
});
if (!loginResponse.success) {
console.error("Failed to login to Bluesky");
process.exit(1);
}
const server = new Server(
{
name: "Bluesky MCP Server",
version,
},
{
capabilities: {
tools: {},
},
},
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools,
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
return handleToolCall(name, agent, args);
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(
`Invalid arguments: ${error.errors
.map((e) => `${e.path.join(".")}: ${e.message}`)
.join(", ")}`,
);
}
throw error;
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});
```
--------------------------------------------------------------------------------
/src/tools/index.ts:
--------------------------------------------------------------------------------
```typescript
import type { AtpAgent } from "@atproto/api";
import { deleteFollowTool, handleDeleteFollow } from "./delete-follow.js";
import { deleteLikeTool, handleDeleteLike } from "./delete-like.js";
import { deletePostTool, handleDeletePost } from "./delete-post.js";
import { deleteRepostTool, handleDeleteRepost } from "./delete-repost.js";
import { followTool, handleFollow } from "./follow.js";
import { getFollowersTool, handleGetFollowers } from "./get-followers.js";
import { getFollowsTool, handleGetFollows } from "./get-follows.js";
import { getLikesTool, handleGetLikes } from "./get-likes.js";
import { getPostThreadTool, handleGetPostThread } from "./get-post-thread.js";
import { getProfileTool, handleGetProfile } from "./get-profile.js";
import { getTimelineTool, handleGetTimeline } from "./get-timeline.js";
import { handleLike, likeTool } from "./like.js";
import { handlePost, postTool } from "./post.js";
import { handleRepost, repostTool } from "./repost.js";
import { handleSearchPosts, searchPostsTool } from "./search-posts.js";
export const tools = [
deleteFollowTool,
deleteLikeTool,
deletePostTool,
deleteRepostTool,
followTool,
getFollowersTool,
getFollowsTool,
getLikesTool,
getPostThreadTool,
getProfileTool,
getTimelineTool,
likeTool,
postTool,
repostTool,
searchPostsTool,
];
export function handleToolCall(
name: string,
agent: AtpAgent,
args?: Record<string, unknown>,
) {
if (name === deleteFollowTool.name) {
return handleDeleteFollow(agent, args);
}
if (name === deleteLikeTool.name) {
return handleDeleteLike(agent, args);
}
if (name === deletePostTool.name) {
return handleDeletePost(agent, args);
}
if (name === deleteRepostTool.name) {
return handleDeleteRepost(agent, args);
}
if (name === followTool.name) {
return handleFollow(agent, args);
}
if (name === getFollowersTool.name) {
return handleGetFollowers(agent, args);
}
if (name === getFollowsTool.name) {
return handleGetFollows(agent, args);
}
if (name === getLikesTool.name) {
return handleGetLikes(agent, args);
}
if (name === getPostThreadTool.name) {
return handleGetPostThread(agent, args);
}
if (name === getProfileTool.name) {
return handleGetProfile(agent, args);
}
if (name === getTimelineTool.name) {
return handleGetTimeline(agent, args);
}
if (name === likeTool.name) {
return handleLike(agent, args);
}
if (name === postTool.name) {
return handlePost(agent, args);
}
if (name === repostTool.name) {
return handleRepost(agent, args);
}
if (name === searchPostsTool.name) {
return handleSearchPosts(agent, args);
}
throw new Error(`Unknown tool: ${name}`);
}
```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
# mcp-server-bluesky
## 0.4.1
### Patch Changes
- [#21](https://github.com/morinokami/mcp-server-bluesky/pull/21) [`c9ad5fb`](https://github.com/morinokami/mcp-server-bluesky/commit/c9ad5fb77c793a49804191bc3316afd5262e9e2c) Thanks [@morinokami](https://github.com/morinokami)! - Sync client version with package.json
## 0.4.0
### Minor Changes
- [#18](https://github.com/morinokami/mcp-server-bluesky/pull/18) [`0b89092`](https://github.com/morinokami/mcp-server-bluesky/commit/0b89092225ac98f20d7f48a9866cef80d46448c6) Thanks [@goodlux](https://github.com/goodlux)! - Add support for configurable PDS URL
## 0.3.0
### Minor Changes
- [#16](https://github.com/morinokami/mcp-server-bluesky/pull/16) [`1b0121c`](https://github.com/morinokami/mcp-server-bluesky/commit/1b0121c3a52d2665d0f826004f5036705675b425) Thanks [@morinokami](https://github.com/morinokami)! - Add `bluesky_repost` and `bluesky_delete_repost`
- [#15](https://github.com/morinokami/mcp-server-bluesky/pull/15) [`4cd040d`](https://github.com/morinokami/mcp-server-bluesky/commit/4cd040d58767c4ea59cd046eb406e6807c414437) Thanks [@morinokami](https://github.com/morinokami)! - Add `bluesky_delete_post`
- [#13](https://github.com/morinokami/mcp-server-bluesky/pull/13) [`9399567`](https://github.com/morinokami/mcp-server-bluesky/commit/93995671add22a3c59aff2d752cf17cee1547080) Thanks [@morinokami](https://github.com/morinokami)! - Add `bluesky_follow` and `bluesky_delete_follow`
- [#17](https://github.com/morinokami/mcp-server-bluesky/pull/17) [`dd0949f`](https://github.com/morinokami/mcp-server-bluesky/commit/dd0949f2ea1fdfa43edbed1e1c986d0a1739bf78) Thanks [@morinokami](https://github.com/morinokami)! - Add `bluesky_search_posts`
## 0.2.0
### Minor Changes
- [#10](https://github.com/morinokami/mcp-server-bluesky/pull/10) [`9981456`](https://github.com/morinokami/mcp-server-bluesky/commit/9981456d6d4331e9290bf11555e27fe89550c826) Thanks [@morinokami](https://github.com/morinokami)! - Add `bluesky_delete_like`
- [#6](https://github.com/morinokami/mcp-server-bluesky/pull/6) [`bc515ec`](https://github.com/morinokami/mcp-server-bluesky/commit/bc515ec3e57b83e96f60e3559f918405b8d619f9) Thanks [@morinokami](https://github.com/morinokami)! - Add `bluesky_post`
- [#4](https://github.com/morinokami/mcp-server-bluesky/pull/4) [`a9296df`](https://github.com/morinokami/mcp-server-bluesky/commit/a9296df4628d92a6c592c5222bebb01c26b307d2) Thanks [@morinokami](https://github.com/morinokami)! - Add `bluesky_get_follows`
- [#11](https://github.com/morinokami/mcp-server-bluesky/pull/11) [`f02c0ae`](https://github.com/morinokami/mcp-server-bluesky/commit/f02c0ae31cfe28aa9b7a4afc168e2ab168d2728c) Thanks [@morinokami](https://github.com/morinokami)! - Add `bluesky_get_post_thread`
- [#7](https://github.com/morinokami/mcp-server-bluesky/pull/7) [`92ea83f`](https://github.com/morinokami/mcp-server-bluesky/commit/92ea83f1c60d3319f41f4dea53e90997bd7882fa) Thanks [@morinokami](https://github.com/morinokami)! - Add `bluesky_get_timeline`
- [#9](https://github.com/morinokami/mcp-server-bluesky/pull/9) [`6daad00`](https://github.com/morinokami/mcp-server-bluesky/commit/6daad00119c93e29980f6906b9d327674843c53a) Thanks [@morinokami](https://github.com/morinokami)! - Add `bluesky_get_likes`
- [#8](https://github.com/morinokami/mcp-server-bluesky/pull/8) [`0c924ca`](https://github.com/morinokami/mcp-server-bluesky/commit/0c924ca82c58cbfbeace81af0ce698245eba202b) Thanks [@morinokami](https://github.com/morinokami)! - Add `bluesky_like`
## 0.1.0
### Minor Changes
- [#2](https://github.com/morinokami/mcp-server-bluesky/pull/2) [`4e4149e`](https://github.com/morinokami/mcp-server-bluesky/commit/4e4149e38e19e900295277cf8c8e8ac0bd6ed492) Thanks [@morinokami](https://github.com/morinokami)! - Add `bluesky_get_follows`
```