beyond-mcp
# beyond-mcp
D&D Beyond MCP server: character sheets, monster statblocks, campaign rosters, a live
game-log roll feed, and opt-in sheet writes. Extracted from
[roll20-dm-mcp](https://github.com/eschatus/roll20-dm-mcp) per its issue #171 so DDB rides
behind its own MCP seam — the dm-whisper gem and Claude Code select it by config, next to
interchangeable VTT-primitive servers (roll20-dm-mcp today, forge-mcp/ddb-maps-mcp later).
**This server knows nothing about Roll20 or any VTT**, and it never opens a browser. Reads
are always on; writes are opt-in (`DDB_WRITES=enabled`).
> Formerly `ddb-mcp`; renamed to avoid a collision with the unrelated public
> [ddb-mcp/ddb-mcp](https://github.com/ddb-mcp/ddb-mcp). Old repo URLs redirect.
## Tools
| Tool | Does |
|---|---|
| `ddb_ping` | Handshake: `{pong, server, version, capabilities}`. Call on connect to assert a minimum version (semver) and see which capabilities (writes/rollPump/partySnapshot) this instance exposes. |
| `ddb_get_character` | Character sheet by numeric id — compact HP/AC/conditions snapshot; `full:true` adds the parsed stat sheet (abilities, saves, skills, initiative). |
| `ddb_get_monster` | Compendium monster by name or id — compact HP/AC/CR snapshot; `full:true` returns the whole statblock. |
| `ddb_list_campaigns` | Every campaign the account is in, with ids. |
| `ddb_list_campaign_characters` | `{id, name}` roster of a campaign. |
| `start_ddb_roll_pump` | Subscribe to a campaign's game-log WebSocket; capture fulfilled dice rolls into a bounded buffer. Skips Beyond20-bridged rolls by default (gap-fill mode). |
| `ddb_poll_rolls` | Cursor-based drain of captured rolls (raw message + one-line summary). Consumers mirror the actual dice values — never re-roll. |
| `stop_ddb_roll_pump` / `ddb_roll_pump_status` | Stop the pump / report connection health (CONNECTED vs RETRYING, failure count, last error). |
Tool names and default output shapes match what roll20-dm-mcp served, so existing callers
migrate by pointing at this server. The roll pump differs by design: it **buffers** instead
of posting to Roll20 — the gem drains it and bridges rolls via roll20-dm-mcp's
`post_roll_as_character`.
### Write tools (opt-in — `DDB_WRITES=enabled`)
Reads are always available; these mutation tools register **only** when the deployment sets
`DDB_WRITES=enabled`, so prep-time and default installs stay read-only. They are primitives —
they effect exactly the change asked on whatever character id they're given, with no policy
about who owns the sheet. Because DDB PC HP is also driven by Beyond20 and the player's own
edits, writes race those (last-writer-wins) and are meant as the DM's deliberate acts.
| Tool | Does |
|---|---|
| `ddb_set_hp` | Set current and/or temp HP by absolute value (clamped to `[0, max]`). |
| `ddb_apply_condition` / `ddb_remove_condition` | Add/remove a 5e condition by name (exhaustion takes a level). |
## Auth
Reads authenticate with the D&D Beyond `CobaltSession` cookie (exchanged for a short-lived
JWT; see [docs/ddb-browserless-protocol.md](docs/ddb-browserless-protocol.md)). The cookie
is **consumed, never harvested**: supply `DDB_COBALT` in the env, or point `DDB_DATA_DIR`
at the directory where the gem writes `ddb-cobalt.json`. Absent or expired cookie ⇒ every
read fails loudly with re-harvest instructions.
## Config
| Env | Meaning |
|---|---|
| `DDB_COBALT` | CobaltSession cookie value (overrides the data file). |
| `DDB_DATA_DIR` | Directory holding `ddb-cobalt.json` + caches. Point at the gem's data dir (roll20-dm-mcp's `ROLL20_DATA_DIR`) to share the harvested cookie. Default `./data`. |
| `DDB_CAMPAIGN_ID` | Default campaign for `ddb_list_campaign_characters` / `start_ddb_roll_pump`. |
| `DDB_WRITES` | Set to `enabled` to register the write tools (default: reads only). |
| `DDB_HTTP_PORT` / `DDB_HTTP_HOST` / `DDB_MCP_TOKEN` | HTTP entry only (defaults 39210 / 127.0.0.1 / auto-generated into `.env`). |
## Running
```sh
npm install
npm run build
npm start # stdio MCP server (Claude Code .mcp.json, gem supervised spawn)
npm run serve # HTTP MCP server on /mcp + rolls SSE on /events (bearer-authed)
```
Claude Code `.mcp.json`:
```json
{
"mcpServers": {
"ddb": { "command": "node", "args": ["E:/personalProjects/beyond-mcp/dist/index.js"] }
}
}
```
## Development
```sh
npm test # vitest run
npx vitest run src/bridge/ddb-gamelog.test.ts # one file
npm run lint
npm run dev # tsx watch, stdio
```
Node 20+, TypeScript (NodeNext ESM), vitest — same conventions as roll20-dm-mcp.
TDQS
Scored across 10 tools
Tools are generally distinct: monster lookup, character lookup, campaign listing, party snapshot, and roll pump management each serve clear purposes. Some overlap exists between ddb_get_character and ddb_get_party_snapshot, but the party snapshot is clearly positioned as the authoritative source for live combat stats, and the descriptions resolve ambiguity.
Most tools follow the ddb_verb_noun pattern (ddb_get_monster, ddb_list_campaigns, ddb_poll_rolls). Minor deviation: start_ddb_roll_pump and stop_ddb_roll_pump put the verb first, breaking the prefix convention. Still, the pattern is predictable overall.
With 10 tools, the server is well-scoped for its purpose: a few core read operations (monster, character, campaigns), a party snapshot, and a roll pump subsystem with start/stop/status/poll. Each tool has a clear role with no redundancy or bloat.
The surface covers the expected read-only operations for a D&D Beyond integration: retrieving monsters, characters, campaigns, and party combat data, plus the roll pump lifecycle. Missing write operations (e.g., updating characters) are not implied by the domain, and the roll pump is fully supported with start, stop, status, and polling.