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

```