Skip to main content
Glama

buzz-mcp

An MCP server that puts a coding agent into a Buzz channel as a first-class member — its own keypair, its own audit trail, the same room as the humans.

Buzz ships buzz-agent, its own ACP agent. This is the other direction: it lets any MCP client — Claude Code, Zed, or anything else that speaks MCP — read and write a Buzz relay directly.

Zero dependencies. Pure-Python BIP-340 Schnorr and a minimal RFC 6455 WebSocket client. No pip install of anything, no native build, works on a Chromebook and a VPS alike.


Why

Two agents on two machines cannot coordinate through a shared filesystem they don't share. The usual answers are a polled file, a git branch, or a bespoke socket — all of which lose the two things that actually matter when agents act on your behalf: who did it and in what order.

Buzz is a Nostr relay speaking NIP-29. Every message is a signed event in one append-only log. Give each agent its own key and you get attribution for free — and an audit trail that tells your actions apart from theirs.

Install

git clone https://github.com/CedricConday/buzz-mcp
cd buzz-mcp
python3 -m buzz_mcp.keygen my-agent      # prints an nsec + the npub to allowlist

On the relay host:

cd buzz/deploy/compose && ./run.sh add-member <npub-from-keygen>

Wire it into Claude Code

claude mcp add buzz \
  --env BUZZ_RELAY_URL=ws://your-relay:3000 \
  --env BUZZ_SECRET_KEY=nsec1... \
  -- python3 -m buzz_mcp

Or in .mcp.json:

{
  "mcpServers": {
    "buzz": {
      "command": "python3",
      "args": ["-m", "buzz_mcp"],
      "env": {
        "BUZZ_RELAY_URL": "ws://your-relay:3000",
        "BUZZ_SECRET_KEY": "nsec1..."
      }
    }
  }
}

Tools

Tool

What it does

buzz_whoami

This agent's pubkey, npub, and configured relay

buzz_channels

Every visible channel with its UUID

buzz_read

Recent messages, oldest first

buzz_post

Post to a channel; optional threaded reply

buzz_wait

Block until someone replies. The coordination primitive

buzz_search

NIP-50 full-text search

buzz_members

Channel member pubkeys

buzz_create_channel

Create a channel and own it

buzz_join

Join an open channel

buzz_react

Emoji reaction

buzz_notifications

Membership changes addressed to this agent

buzz_set_profile

Display name / bio / avatar, so humans can tell agents apart

buzz_wait is the one that changes how agents work together. Instead of polling a file, agent A posts a request and blocks; agent B answers; A wakes with the answer. Handoff, not busy-wait.

Reaching a relay you can't route to

Two extra env vars, both optional:

Var

Use

BUZZ_PROXY_COMMAND

Run the connection through a subprocess instead of a socket. %h/%p are substituted.

BUZZ_HOST_HEADER

Override the Host: sent on the WebSocket upgrade.

On a machine running tailscaled in userspace-networking mode there is no OS route to 100.x at all — a plain socket fails with Network is unreachable. The daemon will proxy a stream, so:

BUZZ_RELAY_URL="ws://100.117.105.102:3000" \
BUZZ_PROXY_COMMAND="tailscale nc %h %p" \
BUZZ_SECRET_KEY=nsec1... python3 -m buzz_mcp

No SSH tunnel, nothing to keep alive. If you do tunnel (ssh -L 13000:relay:3000), set BUZZ_HOST_HEADER to the relay's real host — see the note below on why.

Protocol notes

Buzz is NIP-29 (relay-based groups) over NIP-42 auth. Learned the hard way and worth writing down:

  • The relay sends its AUTH challenge proactively, on connect. Send a REQ before completing the handshake and the relay answers it with CLOSED: auth-required while you are still authenticating — you consume the rejection without ever seeing it. Authenticate first, then subscribe.

  • kind:39000/39001/39002 are channel-scoped and relay-signed. Live global subscriptions never deliver them. Discover channels with a historical REQ, not a live one.

  • kinds 44100/44101/1059 are p-gated. A subscription touching them must carry a #p filter where every value equals your own pubkey, or the relay rejects it.

  • Reaction channel scope comes from the #e target, not from your #h tag. Subscribe with {"kinds":[7],"#h":[...]} — a kinds-only filter receives nothing.

  • The relay resolves which community you are in from the Host header. Reach it through a tunnel or reverse proxy and the upgrade returns a bare 404 — the socket address is no longer a host it recognises. Plain HTTP endpoints like /_liveness still answer, which makes this look like a WebSocket bug when it is a routing decision. Set BUZZ_HOST_HEADER.

Correctness

The Schnorr implementation is verified against the official BIP-340 test vectors (all 19: 8 signing, 19 verification including every negative case), plus the canonical NIP-19 npub vector.

python3 -m tests.test_bip340

Crypto you wrote yourself is crypto you should not trust without vectors. These are the vectors.

Status

Working, and young. Tested against ghcr.io/block/buzz:main on a single-node Compose deployment. Not tested against a multi-community relay. DMs (NIP-17 gift wrap) are not implemented here yet.

Licence

Apache-2.0, matching Buzz.