atpmcp

A local MCP server for AT Protocol development and debugging.

Communicates via stdio using JSON-RPC 2.0, enabling AI assistants to work with AT Protocol records, identities, and lexicons. Exposes nine tools for computing content identifiers, resolving handles and DIDs, validating lexicon schemas, parsing rich text facets, retrieving records, describing lexicons, validating XRPC requests, and invoking XRPC methods.

Tools

create_record_cid
Serializes a JSON record to deterministic DAG-CBOR, hashes it with SHA-256, and returns a CIDv1 base32 string.
validate_lexicon_schema
Validates an AT Protocol lexicon schema, checking version, NSID, and definition structure.
resolve_handle_to_did
Resolves an AT Protocol handle (e.g. alice.bsky.social) to its DID identifier.
resolve_identity
Retrieves a full DID document for a given DID, with optional PLC directory override.
parse_facets
Extracts rich text facets (mentions, URLs, hashtags) from plain text with UTF-8 byte offsets, resolving mentions to DIDs when possible.
get_record
Retrieves an AT Protocol record by AT-URI, resolving identity and PDS endpoint automatically.
get_lexicon
Retrieves and describes an AT Protocol lexicon schema by NSID, returning its definitions, required fields, and structure.
validate_xrpc
Validates XRPC request parameters and body against an AT Protocol lexicon schema, catching missing or invalid input before making a call.
invoke_xrpc
Invokes an authenticated XRPC method on a PDS, supporting both query and procedure calls with optional parameters and request body.

Example Prompts

Compute a CID

> Compute the CID for this post record: {"text": "Hello world", "$type": "app.bsky.feed.post", "createdAt": "2024-01-01T00:00:00Z"}

atpmcp - create_record_cid (MCP)
  record: {"text":"Hello world","$type":"app.bsky.feed.post","createdAt":"2024-01-01T00:00:00Z"}
  bafyreigdbtyg3kz4i3ci4zaym5uphiixi5dwca7ddfop2xlvx3m7rpjzxe

The CID for that post record is:
  bafyreigdbtyg3kz4i3ci4zaym5uphiixi5dwca7ddfop2xlvx3m7rpjzxe

Resolve a handle

> Resolve the handle ngerakines.me to a DID.

atpmcp - resolve_handle_to_did (MCP)
  handle: "ngerakines.me"
  did:plc:cbkjy5n7bk3ax2wplmtjofq2

The handle ngerakines.me resolves to did:plc:cbkjy5n7bk3ax2wplmtjofq2.

Resolve an identity

> Get the full DID document for did:plc:cbkjy5n7bk3ax2wplmtjofq2.

atpmcp - resolve_identity (MCP)
  did: "did:plc:cbkjy5n7bk3ax2wplmtjofq2"
  {"@context": [...], "id": "did:plc:cbkjy5n7bk3ax2wplmtjofq2", ...}

Here's the full DID document for did:plc:cbkjy5n7bk3ax2wplmtjofq2:
  - ID: did:plc:cbkjy5n7bk3ax2wplmtjofq2
  - Also Known As:
    - at://ngerakines.me
    - at://nick.gerakines.net
    - at://nick.thegem.city
    - https://github.com/ngerakines
    - https://ngerakines.me/
    - dns:ngerakines.me
  - Service:
    - PDS: https://pds.cauda.cloud (AtprotoPersonalDataServer)
  - Verification Method:
    - Type: Multikey
    - Public Key (multibase): zQ3shXvCK2RyPrSLYQjBEw5CExZkUhJH3n1K2Mb9sC7JbvRMF

Fetch a record

> Fetch the record at at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/app.bsky.feed.post/3mgn37sytoc2c.

atpmcp - get_record (MCP)
  uri: "at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/app.bsky.feed.post/3mgn37sytoc2c"
  {"$type": "app.bsky.feed.post", "createdAt": "2026-03-09T14:34:06.694Z", ...}

Here's the record:
  - Type: app.bsky.feed.post
  - Created: 2026-03-09T14:34:06.694Z
  - Language: English
  - Text:
    atpmcp is a local MCP server that is part of the atproto-crates project.
    It supports the same authentication as the atpxrpc tool and is using the
    latest MCP features.

    Install with homebrew: brew tap ngerakines/tap && brew install atpmcp

    Read more at ngerakines.me/atpmcp.html
  - Embed: External link to atpmcp
  - Facet: Link facet on "ngerakines.me/atpmcp.html" (bytes 252-277)

Parse facets

> Parse the facets from this text: Hello @alice.bsky.social, check out https://atproto.com #atdev

atpmcp - parse_facets (MCP)
  text: "Hello @alice.bsky.social, check out https://atproto.com #atdev"
  [{"index": {"byteStart": 6, "byteEnd": 24}, ...}, ...]

Three facets were parsed from the text:
  1. Mention (bytes 6-24): @alice.bsky.social resolved to did:plc:vdjlpwlhbnug4fnjodwr3vzh
  2. Link (bytes 36-55): https://atproto.com
  3. Tag (bytes 56-62): #atdev

Validate a lexicon

> Validate this lexicon schema and tell me if there are any issues: {
    "id": "community.lexicon.bookmarks.bookmark",
    "defs": { "main": { "key": "tid", "type": "record", ... } },
    "$type": "com.atproto.lexicon.schema",
    "lexicon": 1
  }

atpmcp - validate_lexicon_schema (MCP)
  schema: {"id":"community.lexicon.bookmarks.bookmark", ...}
  Lexicon schema 'community.lexicon.bookmarks.bookmark' is valid.

The schema is valid. The lexicon community.lexicon.bookmarks.bookmark is well-formed with:
  - A proper NSID
  - Lexicon version 1
  - A record type with tid key
  - Required fields subject (URI format) and createdAt (datetime format)
  - An optional tags array of strings

Create a record

> Get the app.bsky.feed.post lexicon schema and draft a post with the text "Hello from atpmcp".

atpmcp - get_lexicon (MCP)
  nsid: "app.bsky.feed.post"
  {"id": "app.bsky.feed.post", "defs": { ... }}

Based on the lexicon, an app.bsky.feed.post record requires two fields: text
and createdAt. Here's the drafted post record:

  {
    "$type": "app.bsky.feed.post",
    "text": "Hello from atpmcp",
    "createdAt": "2026-03-09T00:00:00.000Z",
    "langs": ["en"]
  }

Key schema details:
  - text (required): string, max 3000 bytes / 300 graphemes
  - createdAt (required): datetime string (client-declared timestamp)
  - langs (optional): language tags, included "en" for clarity
  - facets (optional): for mentions, URLs, hashtags
  - embed (optional): for images, video, external links, or quote posts
  - reply (optional): root + parent strong refs for threading

> Publish this to my PDS using com.atproto.repo.createRecord and invoke_xrpc as ngerakines.me.

atpmcp - invoke_xrpc (MCP)
  nsid: "com.atproto.repo.createRecord"
  auth_handle: "ngerakines.me"
  body: {"repo":"ngerakines.me","collection":"app.bsky.feed.post","record":{...}}
  {"uri": "at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/app.bsky.feed.post/3mgniby6dyf2i", ...}

Post published successfully!
  - AT-URI: at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/app.bsky.feed.post/3mgniby6dyf2i
  - CID: bafyreigrnwyhnc3bftgpuhvonqlirpbikgc4purstawljuwhysusrhymxu
  - Validation: valid

Validate XRPC requests

> Use the com.atproto.identity.resolveHandle lexicon to validate a request with the
  parameter handle set to "ngerakines.me". Then try validating it again with an empty
  handle to see how validation catches missing or invalid input.

atpmcp - validate_xrpc (MCP)
  nsid: "com.atproto.identity.resolveHandle"
  params: {"handle":"ngerakines.me"}
  Validation passed for query com.atproto.identity.resolveHandle: params validated successfully.

atpmcp - validate_xrpc (MCP)
  nsid: "com.atproto.identity.resolveHandle"
  params: {"handle":""}
  Error: XRPC validation failed: params: Invalid handle format for '': handle cannot be empty

Install

brew tap ngerakines/tap
brew install atpmcp

Configure

Add atpmcp to your Claude Code MCP configuration at ~/.claude/claude_code_config.json:

{
  "mcpServers": {
    "atpmcp": {
      "command": "atpmcp",
      "args": []
    }
  }
}