beyond-mcp
beyond-mcp is a read-first D&D Beyond MCP server that answers character, monster, campaign, and live dice-roll questions using D&D Beyond's own numbers — no LLM, no browser, no VTT knowledge.
Handshake —
ddb_pingreports server name, semver version, and capability flags (writes,rollPump,partySnapshot); assert a minimum version before relying on newer tools.Character sheets —
ddb_get_characterfetches a character by numeric id (compact HP/AC/conditions by default;full:trueadds ability scores/mods, saves, skills, initiative, speed).Authoritative party combat snapshot —
ddb_get_party_snapshotreturns precomputed AC and max/current/temp HP, conditions, death saves, initiative, passives and level for a whole party in one batched SCDS call; passcharacterIdsor acampaignId(defaults toDDB_CAMPAIGN_ID). Prefer this overddb_get_characterfor live status.full:trueadds attacks, casting, abilities, skills, senses, resistances.Monster statblocks —
ddb_get_monsterlooks up a compendium monster by name or id (compact HP/AC/CR by default;full:truereturns the whole statblock including traits/actions/reactions, resistances/immunities, art URL).Campaign discovery —
ddb_list_campaignslists every campaign the account is in (DM or player) with ids and player counts;ddb_list_campaign_characterslists the{id, name}roster for a campaign (defaults toDDB_CAMPAIGN_ID).Live dice-roll pump —
start_ddb_roll_pumpsubscribes browserless to a campaign's game-log WebSocket (table-wide or named characters; skips Beyond20-bridged rolls unlessincludeBeyond20:true);ddb_poll_rollscursor-drains captured rolls (seq, summary, raw message,missed, pump health);stop_ddb_roll_pumpdisconnects;ddb_roll_pump_statusreports CONNECTED vs RETRYING, failures, capture count, last error. Rolls must be mirrored, never re-rolled.Opt-in writes (not registered in this instance) — when
DDB_WRITES=enabled,ddb_set_hp,ddb_apply_condition, andddb_remove_conditionbecome available; the schema shown exposes only reads and the roll pump.Auth via one credential — consumes a
CobaltSessioncookie (DDB_COBALTorddb-cobalt.json), exchanges it for a 300s JWT, refreshes with margin, and fails loudly withCobaltUnavailableError— it never harvests cookies or opens a browser.Transports — stdio MCP, or HTTP (
npm run serve) on:39210/mcpwith bearer auth plus an SSE roll feed at/events.
Mentions Roll20 as a target VTT: the server buffers raw D&D Beyond game-log dice rolls so the caller can bridge/post them into Roll20 chat (via an adapter), while the server itself is VTT-agnostic and posts nothing directly.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@beyond-mcp get the statblock for a young red dragon"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
beyond-mcp
A read-first D&D Beyond MCP server: character sheets, monster statblocks, campaign rosters, a precomputed party combat snapshot, a live game-log dice feed, and opt-in sheet writes. Browserless, VTT-agnostic, MIT.
It is a primitive, not an assistant. It has no LLM inside it, knows nothing about Roll20 or Foundry, and never opens a browser. Point any MCP client at it — Claude Code, an agent framework, your own harness — and it answers D&D Beyond questions with D&D Beyond's own numbers.
┌─ character sheets (character-service v5)
├─ monsters (monster-service v1)
MCP client ──▶ beyond-mcp ──┼─ party snapshot (SCDS, batched)
(stdio or HTTP) │ └─ live dice rolls (game-log WebSocket)
└─ auth: CobaltSession cookie ─▶ 300s JWTFormerly
ddb-mcp; renamed before going public to avoid a collision with the unrelated ddb-mcp/ddb-mcp. Tool names (ddb_*), env vars (DDB_*) and theddb-cobalt.jsonfilename were deliberately left unchanged.
Three stances that shape everything else
It consumes a session cookie; it never harvests one. Cookie capture is interactive — it
needs a logged-in browser and a human when that fails — so it belongs in whatever app has a
user in front of it. This server takes the cookie from DDB_COBALT or a file on disk and
fails loudly when it is missing or expired. There is no Playwright dependency, no silent
fallback, and no credential-capture surface here at all — which is what made publishing it
reasonable.
It knows nothing about any VTT. The roll pump buffers raw game-log messages; posting
one into Roll20 chat or a Foundry board is the caller's job. Helpers stay neutral
(summarizeRoll). Consumers put an adapter in front of this server rather than asking it to
be generic — a half-generic server is a leaky shared kernel, where one VTT's idioms escape
into something labelled neutral and the next backend inherits them.
Reads are always on; writes are opt-in and are primitives. The three mutation tools
register only when the deployment sets DDB_WRITES=enabled, so a default install and
any prep-time use stay read-only. When enabled they effect exactly the change asked on
whatever character id they are given, with no policy about who owns the sheet — the caller
owns intent. D&D Beyond PC HP already has two writers the table can see (Beyond20 and the
player), so a write here races them, last-writer-wins. That makes them deliberate acts, not
an automation loop.
Related MCP server: dndbeyond-mcp
Quick start
npm install
npm run build
export DDB_COBALT='<your CobaltSession cookie value>'
npm start # stdio MCP server
# or
npm run serve # HTTP MCP on :39210/mcp, rolls SSE on /eventsClaude Code (.mcp.json):
{
"mcpServers": {
"beyond": {
"command": "node",
"args": ["/path/to/beyond-mcp/dist/index.js"],
"env": { "DDB_COBALT": "...", "DDB_CAMPAIGN_ID": "1234567" }
}
}
}Call ddb_ping first — it reports the version and which capabilities this instance exposes.
Tools
Handshake
Tool | Returns |
|
|
Reads (always registered)
Tool | Does |
| Character sheet by numeric id. Default: compact HP / AC / temp HP / conditions. |
| The authoritative combat read. One batched call to D&D Beyond's SCDS service returns the site's own precomputed AC, max/current/temp HP, named conditions, death saves, initiative, passives and level for a whole party. |
| Compendium monster by name or numeric id. Default: compact HP / AC / CR. |
| Every campaign the account is in, as DM or player, with ids and player counts. |
|
|
Which character read to use
Prefer ddb_get_party_snapshot for anything the table acts on. The raw v5 character sheet
carries neither AC nor max HP in computed form, so the compact ddb_get_character path
derives max HP from the character's raw CON, before racial, item and override
modifiers — which undercounts for any boosted CON (verified live against a level-14
Barbarian: 117 against SCDS's 131, a gap matching exactly (final CON mod − raw CON mod) × level), and falls back to AC 10 where the payload carries no computed armour class at all.
Tracked as #5; the snapshot is the
documented answer. Both bugs predate this repo — inherited at extraction, and only caught
when the snapshot work forced a side-by-side comparison.
Roll pump (always registered)
Subscribes browserless to a campaign's game-log WebSocket and captures fulfilled dice rolls. Built for the orphaned-character case: a player rolls on D&D Beyond with no Beyond20 bridge, so their rolls never reach the table.
Tool | Does |
| Arm the pump on a campaign, table-wide or for named characters. Skips rolls Beyond20 already bridged ( |
| Cursor drain: each roll's |
| Disconnect. Buffered rolls stay drainable until the next start. |
|
|
Two details that matter to consumers:
Rolls carry D&D Beyond's actual dice values. Mirror them; never re-roll. Re-rolling turns a player's crit into some other number on the board.
The buffer is deliberately shallow — 10 rolls, then oldest-evicts. These are live table events, and a consumer returning from a long stall must not flood chat with stale rolls.
ddb_poll_rollsreportsmissedrather than silently skipping them.
Writes (only when DDB_WRITES=enabled)
Tool | Does |
| Set current and/or temp HP by absolute value — current clamped to |
| Apply a 5e condition by name (exhaustion takes |
| Remove one by name. |
Every write route's body shape is verified live against character-service before its tool
ships — POST an empty body, and the 400 names the required fields. No wrapper is added
for an unverified route.
Auth
One credential: the long-lived D&D Beyond CobaltSession cookie, exchanged at
auth-service/v1/cobalt-token for a JWT with a 300-second TTL, refreshed with a margin so
no call races its expiry. The same JWT authenticates the game-log WebSocket (as ?stt=).
Resolution order is DDB_COBALT, then <data dir>/ddb-cobalt.json. When the cookie is
absent, malformed or rejected, every read fails with a CobaltUnavailableError naming the
file it looked in and how to refresh it. The server never retries a cookie that was just
rejected, and never falls back to a browser.
Getting the cookie
There is no harvester in this repo. Any app with a user in front of it can write the file; otherwise copy the cookie by hand, which takes a minute:
Log in at
www.dndbeyond.comwith "remember me" ticked — that is the long-lived cookie.Open DevTools → Application (Chrome/Edge) or Storage (Firefox) → Cookies →
https://www.dndbeyond.com, and copy the value ofCobaltSession(without theCobaltSession=prefix). It isHttpOnly, sodocument.cookiewill not show it.Close the tab rather than logging out; logging out is likely to revoke the session.
Check it before wiring anything up — a working cookie exchanges for a JWT:
curl -s -X POST https://auth-service.dndbeyond.com/v1/cobalt-token \
-H "Cookie: CobaltSession=<value>" -H "Content-Type: application/json" -d '{}'
# → {"token":"eyJ…","ttl":300}Then supply it one of two ways:
DDB_COBALTin the MCP client'senvblock (see Quick start). Simplest; changing it means restarting the server.<DDB_DATA_DIR>/ddb-cobalt.jsoncontaining{"cookie": "<value>"}(aharvestedAtepoch-ms field is conventional but unread). Better for rotation: when a cookie is rejected the server re-reads the file and picks up a different value without a restart. SetDDB_DATA_DIRto an absolute path — the default./dataresolves against the working directory, which MCP clients choose for you. The same goes for.env, which is loaded from the working directory, not from beside the server, so prefer the client'senvblock.
When it eventually expires, reads start failing with CobaltUnavailableError (and
ddb_roll_pump_status shows climbing failures) — copy a fresh value and overwrite the file.
Call ddb_list_campaigns to confirm; ddb_ping does not exercise the cookie.
Full protocol notes — the auth chain, every endpoint, the monster-service id→name mapping,
the game-log message shape, and Beyond20's __b20Override__ semantics — are in
docs/ddb-browserless-protocol.md and
docs/ddb-gamelog-pump.md.
Config
Env | Meaning |
| CobaltSession cookie value. Takes precedence over the data file. |
| Directory holding |
| Default campaign for |
|
|
| HTTP entry only. Default |
| HTTP bearer token. Auto-generated into |
See .env.example.
HTTP transport
npm run serve runs a long-lived process that several clients can share: each initialize
mints its own session and McpServer instance, but tool state — the pump and the roll
buffer — is a module-level singleton by design, so one process owns the one game-log
WebSocket.
POST /mcp— streamable HTTP MCP. Bearer-authed, with DNS-rebinding protection on andHost/Originallowlisted to the configured host, so a malicious page cannot rebind a visitor's browser onto the local port.GET /events— SSE stream of captured rolls (event: roll), the push alternative to pollingddb_poll_rolls. Same bearer, 30-second keepalives.
The token check is constant-time. The server binds to 127.0.0.1 by default; expose it
beyond localhost only behind something you trust.
Architecture
src/tools/ddb.ts lookup tools ──▶ src/bridge/dndbeyond.ts domain mapping (parseStats,
src/tools/ddbPump.ts pump tools │ mapRawMonster, HP math)
src/tools/ddbWrite.ts opt-in writes │ src/bridge/ddb-scds.ts batched party snapshot
src/tools/meta.ts ddb_ping ▼
│ src/bridge/ddb-rt.ts auth (cobalt→JWT, ttl 300s) + raw fetches
▼ ▲ + 7-day damageAdjustments overlay cache
src/bridge/roll-buffer.ts cursor ring │
src/bridge/ddb-gamelog.ts WS pump ───────┘
src/bridge/ddb-monster-tables.ts baked id→name tables (CR, alignment, …)Entry points are index.ts (stdio) and index-http.ts (HTTP); both build the same server
via server.ts. Raw D&D Beyond payloads stay any at the transport boundary — typed shapes
begin at the domain mappers in dndbeyond.ts.
The monster lookup tables (CR, alignment, size, movement, condition ids) are baked in,
because they do not change. The one that does — damageAdjustments, which accretes new ids
as D&D Beyond adds content — is fetched into a 7-day cache and overlaid on the baked table,
with the baked table as the offline fallback.
Reconnect backoff resets only on a proven-healthy connection: a frame received, or an
open socket surviving 15 seconds. Never on open alone — that lets a socket which opens and
immediately dies reset the backoff and hammer the server forever. The behaviour is pinned by
a test; preserve it if you touch the pump.
Versioning is a consumer contract
ddb_ping and MCP serverInfo both announce src/version.ts's VERSION. Consumers pin a
tag and floor the version against their own minimum, so a newer server is fine and only
an older one is a problem.
The handshake exists because a paired-constant version lock cannot catch the failure it is
for. npm install against a changed git ref does not re-resolve: the lockfile keeps the old
SHA and node_modules silently serves the old build, so a stale tree and a stale constant
agree and pass green. A lock reports what was fetched; the handshake reports what is
answering.
Releases are tagged vX.Y.Z, so consumers pin github:eschatus/beyond-mcp#v0.4.1 rather
than a bare SHA. Adding a tool is a minor bump; VERSION and package.json move together.
Where it came from
Extracted from roll20-dm-mcp in August 2026,
per that repo's issue #171. That server's competency is the Roll20 Mod relay; the D&D Beyond
bridge rode along inside it as a second reverse-engineered protocol with its own auth,
transport and failure modes. One repo, one protocol — so the bridge got its own, and
roll20-dm-mcp v2.0.0 dropped its ddb_* tools entirely. This is now the only home for these
reads.
Tool names and default output shapes are pinned to what roll20-dm-mcp served, so callers
migrated by pointing at this server. The full flags are additive; omit them and the
payloads are byte-compatible with the originals. Extend additively — never reshape a
default.
Known gaps
Open issues, each with a scoped shape and no live-verified payload behind it yet:
#2
ddb_get_character_actions— the weapon/spell attack list, with to-hit and damage.#3
ddb_get_character_features— class features, feats and racial traits text.#5 — the inherited slim-path max-HP and missing-AC bugs described above.
#7
ddb_lookup_spell— "what is the text of Cloudkill?" without a manual search.
Development
npm test # vitest run
npx vitest run src/bridge/ddb-gamelog.test.ts # one file
npm run lint
npm run dev # stdio, tsx watch
npm run serve:watch # HTTP, tsx watchNode 20+, TypeScript strict, NodeNext ESM (imports end in .js). Tests sit next to the code
as src/**/*.test.ts with mocked transports — no live D&D Beyond calls in CI — and
test/setup.ts isolates DDB_DATA_DIR per worker.
Licence
MIT. See LICENSE.
Not affiliated with, endorsed by, or connected to D&D Beyond, Wizards of the Coast or Fandom. This server speaks to D&D Beyond's own web endpoints using your own account's credential, on your behalf — respect their terms of service and your account's standing.
Available Tools
10 toolsddb_get_characterA
Fetch a character sheet from D&D Beyond by numeric ID. Default: a compact HP/AC/conditions snapshot. full:true adds the parsed sheet (ability scores/mods, saves, skills, initiative, speed).
Inputs: ddbCharId (required), full.
ddb_get_monsterA
Get monster stats from the D&D Beyond compendium by name or numeric id. Default: a compact HP/AC/CR snapshot. full:true returns the whole statblock (ability scores, speed, traits/actions/reactions, resistances/immunities, art URL).
Inputs: nameOrId (required), full.
ddb_get_party_snapshotA
Precomputed combat snapshot for a whole party in one call, via D&D Beyond's SCDS service. Returns each character's COMPUTED armor class and max/current/temp HP (which the plain character sheet does not carry pre-derived), plus named conditions, death saves, initiative, passives and level. This is the authoritative AC/maxHP source — prefer it over ddb_get_character for live party status. full:true adds resolved attacks (to-hit + damage dice), casting info, abilities, skills, senses, resistances/immunities. Pass characterIds, or a campaignId (defaults to DDB_CAMPAIGN_ID) to snapshot its whole roster.
Inputs: full, campaignId, characterIds.
ddb_list_campaign_charactersA
List all player characters in a DDB campaign. campaignId defaults to the DDB_CAMPAIGN_ID env var.
Inputs: campaignId.
ddb_list_campaignsA
List all DnD Beyond campaigns the account is in (as DM or player), with IDs and player counts
Inputs: no named parameters.
ddb_pingA
Handshake: returns this server's name, semver version, and capability flags. Call it on connect and assert version meets your expected minimum (semver) before relying on newer tools; read capabilities to see whether writes are enabled and which optional surfaces (rollPump, partySnapshot) exist. Fields are additive-only across versions.
Inputs: no named parameters.
ddb_poll_rollsA
Drain dice rolls the pump has captured since a cursor. Returns each roll's seq (the cursor), a one-line summary, and the raw DDB game-log message (mirror its actual dice values when bridging — never re-roll). Pass the previous response's cursor as after; start with 0 (or omit) for everything buffered.
Inputs: after.
ddb_roll_pump_statusA
Report the D&D Beyond roll pump's connection health (CONNECTED vs RETRYING, consecutive failures, last error) and how many rolls it has captured. RETRYING with a climbing failure count means the socket keeps dropping — usually an expired DDB session cookie; re-harvest and restart the pump.
Inputs: no named parameters.
start_ddb_roll_pumpA
Capture live D&D Beyond dice rolls from a campaign's game log. Subscribes (browserless) to the DDB game-log WebSocket and buffers each fulfilled roll — drain with ddb_poll_rolls and bridge into your VTT yourself (this server posts nothing anywhere; rolls MUST be mirrored, never re-rolled). By default it SKIPS rolls Beyond20 already bridged to the table (data.b20Override), so what you drain is exactly the gap a flaky Beyond20 left — safe to arm table-wide (omit characterNames) or per-player without double-posting downstream.
Inputs: gameId, characterNames, includeBeyond20.
stop_ddb_roll_pumpA
Stop the D&D Beyond roll pump (disconnect the game-log WebSocket). Buffered rolls remain drainable until the next start.
Inputs: no named parameters.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
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.
Maintenance
Related MCP Connectors
D&D 5e MCP — wraps the D&D 5th Edition API (free, no auth)
Connect any AI to your Foundry VTT world: actors, combat, dice, journals, tokens, compendiums.
Manage your tabletop RPG campaign from any MCP client: worlds, sessions, quests, lore, recaps.
Read and update your SekkeiFlow life boards, counters and AI coach from any MCP client.
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceConnects Claude Desktop to Foundry VTT for AI-powered campaign management, enabling natural language interaction with game data including quest creation, character management, compendium searches, and dice rolling. Provides 20 MCP tools for seamless integration between Claude and your tabletop RPG sessions.85MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI assistants to manage D&D Beyond characters, access campaigns, and look up spells, monsters, items, and more through a reverse-engineered API.MIT
- FlicenseNot gradedqualityDmaintenanceA local MCP server that provides access to D&D Beyond character sheets, SRD content (spells, monsters, items), a dice roller, and optional campaign tools via undocumented endpoints.-
- AlicenseNot gradedqualityCmaintenanceEnables MCP-compatible clients to read D&D Beyond characters, campaigns, and game reference data, with optional opt-in tools for session tracking and character building.MIT