n8's blog

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:

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):

tools (actions): the post tool accepts optional parameters so it handles everything:

# 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:

resources vs tools

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:

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

#ai #atproto #bluesky #mcp #open-source #python