mcp-nativewind
import { Agent, CredentialSession } from "@atproto/api";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
type CallToolRequest,
CallToolRequestSchema,
ListToolsRequestSchema,
type Tool,
} from "@modelcontextprotocol/sdk/types.js";
const getProfileTool: Tool = {
name: "bluesky_get_profile",
description: "Get a user's profile information",
inputSchema: {
type: "object",
properties: {},
},
};
const getPostsTool: Tool = {
name: "bluesky_get_posts",
description: "Get recent posts from a user",
inputSchema: {
type: "object",
properties: {
limit: {
type: "number",
description: "Maximum number of posts to return (default 50, max 100)",
default: 50,
},
cursor: {
type: "string",
description: "Pagination cursor for next page of results",
},
},
},
};
const searchPostsTool: Tool = {
name: "bluesky_search_posts",
description: "Search for posts on Bluesky",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "The search query",
},
limit: {
type: "number",
description: "Maximum number of posts to return (default 25, max 100)",
default: 25,
},
cursor: {
type: "string",
description: "Pagination cursor for next page of results",
},
},
required: ["query"],
},
};
const getFollowsTool: Tool = {
name: "bluesky_get_follows",
description: "Get a list of accounts the user follows",
inputSchema: {
type: "object",
properties: {
limit: {
type: "number",
description:
"Maximum number of follows to return (default 50, max 100)",
default: 50,
},
cursor: {
type: "string",
description: "Pagination cursor for next page of results",
},
},
},
};
const getFollowersTool: Tool = {
name: "bluesky_get_followers",
description: "Get a list of accounts following the user",
inputSchema: {
type: "object",
properties: {
limit: {
type: "number",
description:
"Maximum number of followers to return (default 50, max 100)",
default: 50,
},
cursor: {
type: "string",
description: "Pagination cursor for next page of results",
},
},
},
};
const getLikedPostsTool: Tool = {
name: "bluesky_get_liked_posts",
description: "Get a list of posts liked by the user",
inputSchema: {
type: "object",
properties: {
limit: {
type: "number",
description:
"Maximum number of liked posts to return (default 50, max 100)",
default: 50,
},
cursor: {
type: "string",
description: "Pagination cursor for next page of results",
},
},
},
};
const getPersonalFeedTool: Tool = {
name: "bluesky_get_personal_feed",
description: "Get your personalized Bluesky feed",
inputSchema: {
type: "object",
properties: {
limit: {
type: "number",
description:
"Maximum number of feed items to return (default 50, max 100)",
default: 50,
},
cursor: {
type: "string",
description: "Pagination cursor for next page of results",
},
},
},
};
const searchProfilesTool: Tool = {
name: "bluesky_search_profiles",
description: "Search for Bluesky profiles",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query string",
},
limit: {
type: "number",
description:
"Maximum number of results to return (default 25, max 100)",
default: 25,
},
cursor: {
type: "string",
description: "Pagination cursor for next page of results",
},
},
required: ["query"],
},
};
async function main() {
// Hard-code Bluesky credentials here
const blueskyToken = process.env.BLUESKY_APP_KEY;
const blueskyIdentifier = process.env.BLUESKY_IDENTIFIER;
if (!blueskyToken || !blueskyIdentifier) {
console.error("BLUESKY_APP_KEY and BLUESKY_IDENTIFIER must be set");
process.exit(1);
}
console.log("Starting Bluesky MCP Server...");
const server = new Server(
{
name: "Bluesky MCP Server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
},
);
const session = new CredentialSession(new URL("https://bsky.social"));
const loginResponse = await session.login({
identifier: blueskyIdentifier,
password: blueskyToken,
});
if (!loginResponse.success) {
console.error("Failed to login");
process.exit(1);
}
const agent = new Agent(session);
server.setRequestHandler(
CallToolRequestSchema,
async (request: CallToolRequest) => {
console.log("Received CallToolRequest:", request);
try {
if (!request.params.arguments) {
throw new Error("No arguments provided");
}
switch (request.params.name) {
case "bluesky_get_profile": {
const response = await agent.getProfile({
actor: blueskyIdentifier,
});
return {
content: [{ type: "text", text: JSON.stringify(response) }],
};
}
case "bluesky_get_posts": {
const { limit, cursor } = request.params.arguments;
const response = await agent.getAuthorFeed({
actor: blueskyIdentifier,
limit: limit as number | undefined,
cursor: cursor as string | undefined,
});
return {
content: [{ type: "text", text: JSON.stringify(response) }],
};
}
case "bluesky_search_posts": {
const { query, limit, cursor } = request.params.arguments;
if (!query) {
throw new Error("Missing required argument: query");
}
const response = await agent.app.bsky.feed.searchPosts({
q: query as string,
limit: limit as number | undefined,
cursor: cursor as string | undefined,
});
return {
content: [{ type: "text", text: JSON.stringify(response) }],
};
}
case "bluesky_get_follows": {
const { limit, cursor } = request.params.arguments;
const response = await agent.getFollows({
actor: blueskyIdentifier,
limit: limit as number | undefined,
cursor: cursor as string | undefined,
});
return {
content: [{ type: "text", text: JSON.stringify(response) }],
};
}
case "bluesky_get_followers": {
const { limit, cursor } = request.params.arguments;
const response = await agent.getFollowers({
actor: blueskyIdentifier,
limit: limit as number | undefined,
cursor: cursor as string | undefined,
});
return {
content: [{ type: "text", text: JSON.stringify(response) }],
};
}
case "bluesky_get_liked_posts": {
const { limit, cursor } = request.params.arguments;
const response = await agent.getActorLikes({
actor: blueskyIdentifier,
limit: limit as number | undefined,
cursor: cursor as string | undefined,
});
return {
content: [{ type: "text", text: JSON.stringify(response) }],
};
}
case "bluesky_get_personal_feed": {
const { limit, cursor } = request.params.arguments;
const response = await agent.getTimeline({
limit: limit as number | undefined,
cursor: cursor as string | undefined,
});
return {
content: [{ type: "text", text: JSON.stringify(response) }],
};
}
case "bluesky_search_profiles": {
const { query, limit, cursor } = request.params.arguments;
if (!query) {
throw new Error("Missing required argument: query");
}
const response = await agent.api.app.bsky.actor.searchActors({
q: query as string,
limit: limit as number | undefined,
cursor: cursor as string | undefined,
});
return {
content: [{ type: "text", text: JSON.stringify(response.data) }],
};
}
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
} catch (error) {
console.error("Error executing tool:", error);
return {
content: [
{
type: "text",
text: JSON.stringify({
error: error instanceof Error ? error.message : String(error),
}),
},
],
};
}
},
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
console.log("Received ListToolsRequest");
return {
tools: [
getProfileTool,
getPostsTool,
searchPostsTool,
getFollowsTool,
getFollowersTool,
getLikedPostsTool,
getPersonalFeedTool,
searchProfilesTool,
],
};
});
const transport = new StdioServerTransport();
console.log("Connecting server to transport...");
await server.connect(transport);
console.log("Bluesky MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});