an atproto mcp server
i've been using claude code a lot lately - anthropic's terminal-based MCP client that lets claude use bash and MCP servers to do software engineering or really any computer-based tasks.
sometimes there's something cool that comes up while working on open source projects that I want to share, but it's a lot of work compiling a summary. so it'd be cool to be able to just generally direct claude to post about something we were just working on.
so i built an mcp server using atproto and fastmcp so i can give it to claude, who already knows what i've been working on intimately (if not just doing things myself).
MCP
MCP is how claude talks to external tools/resources.
"MCP?" here's an over-simplified summary:
- MCP server == toolbox for interacting with something in the world (e.g. Linear, Github, my Phillips Hue lights)
- MCP client == interpreter of user intent that will invoke servers as needed (e.g. claude code, cursor, Jarvis in my future smart glasses)
in terms of the "agentic" language popular over the last few years, you can generally think of MCP servers as collections of "tools" and MCP clients as "agents" that may decide to use one or many servers as a tool, and that's not so incorrect
fastmcp makes building these servers trivial with decorators:
from fastmcp import FastMCP
mcp = FastMCP("my-server")
@mcp.tool
def greet(name: str) -> str:
return f"hello {name}!"
what we built today
an MCP server for bluesky's at protocol with:
resources (read-only):
atproto://profile/status
atproto://timeline
atproto://notifications
tools (actions):
the post
tool accepts optional parameters so it handles everything:
post
- handles all posting scenariossearch
- find postsfollow
,like
,repost
- social actions
# simple
{"text": "hello"}
# with image
{"text": "check this", "images": ["url.jpg"]}
# reply with rich text
{
"text": "see this article",
"reply_to": "at://...",
"links": [{"text": "this article", "url": "..."}]
}
# quote + image
{
"text": "context:",
"quote": "at://...",
"images": ["chart.png"]
}
architecture
atproto_mcp/
āāā server.py # mcp decorators
āāā _atproto/ # implementation
ā āāā _posts.py # unified posting
ā āāā _client.py # auth
ā āāā _read.py # timeline, notifs
ā āāā _social.py # follow, like
āāā types.py # typedicts
originally had separate tools for each posting variant (post
, post_with_images
, reply
, etc). consolidated to one tool with optional params - much cleaner api.
adding this to your favorite MCP client
to give claude code access to this MCP server
claude mcp add atproto -- uvx atproto-mcp@git+https://github.com/jlowin/fastmcp.git#subdirectory=examples/atproto_mcp
or for Claude Desktop, add this to your claude_desktop_config.json
{
"mcpServers": {
"atproto": {
"command": "uvx",
"args": ["atproto-mcp@git+https://github.com/jlowin/fastmcp.git#subdirectory=examples/atproto_mcp"],
"env": {
"ATPROTO_HANDLE": "your.handle",
"ATPROTO_PASSWORD": "app-password"
}
}
}
}
now you can:
- "post about the bug i just fixed"
- "search for posts from @why.bsky.team about raspberry pi"
- "go jokingly antagonize @void.comind.network by being a contrarian with bad opinions"
resources vs tools
- resources: things you fetch by uri
- tools: actions with parameters
i initially made search a resource (atproto://search/{query}
) but that seemed weird. search takes parameters, seems like a tool.
setup
git clone https://github.com/jlowin/fastmcp.git
cd fastmcp/examples/atproto_mcp
echo 'ATPROTO_HANDLE=you.bsky.social' >> .env
echo 'ATPROTO_PASSWORD=app-password' >> .env
uv run python demo.py --post
extending
could add:
- improved thread posting (review before posting etc)
- scheduling
- media generation before posting
- cross-posting
since it's mcp, any client that supports the protocol can use it - claude code, claude desktop, or cursor/windsurf.
check out the code: github.com/jlowin/fastmcp/examples/atproto_mcp