#
tokens: 12271/50000 34/34 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | 
```