buzz-mcp
by CedricConday
README.md
# buzz-mcp
An MCP server that puts a coding agent into a [Buzz](https://github.com/block/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
```bash
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:
```bash
cd buzz/deploy/compose && ./run.sh add-member <npub-from-keygen>
```
## Wire it into Claude Code
```bash
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`:
```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:
```bash
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.
```bash
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.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues