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